104 lines
3.5 KiB
Python
104 lines
3.5 KiB
Python
|
import sys
|
||
|
|
||
|
|
||
|
def immutable(members='', name='Immutable', verbose=False):
|
||
|
"""
|
||
|
Produces a class that either can be used standalone or as a base class for persistent classes.
|
||
|
|
||
|
This is a thin wrapper around a named tuple.
|
||
|
|
||
|
Constructing a type and using it to instantiate objects:
|
||
|
|
||
|
>>> Point = immutable('x, y', name='Point')
|
||
|
>>> p = Point(1, 2)
|
||
|
>>> p2 = p.set(x=3)
|
||
|
>>> p
|
||
|
Point(x=1, y=2)
|
||
|
>>> p2
|
||
|
Point(x=3, y=2)
|
||
|
|
||
|
Inheriting from a constructed type. In this case no type name needs to be supplied:
|
||
|
|
||
|
>>> class PositivePoint(immutable('x, y')):
|
||
|
... __slots__ = tuple()
|
||
|
... def __new__(cls, x, y):
|
||
|
... if x > 0 and y > 0:
|
||
|
... return super(PositivePoint, cls).__new__(cls, x, y)
|
||
|
... raise Exception('Coordinates must be positive!')
|
||
|
...
|
||
|
>>> p = PositivePoint(1, 2)
|
||
|
>>> p.set(x=3)
|
||
|
PositivePoint(x=3, y=2)
|
||
|
>>> p.set(y=-3)
|
||
|
Traceback (most recent call last):
|
||
|
Exception: Coordinates must be positive!
|
||
|
|
||
|
The persistent class also supports the notion of frozen members. The value of a frozen member
|
||
|
cannot be updated. For example it could be used to implement an ID that should remain the same
|
||
|
over time. A frozen member is denoted by a trailing underscore.
|
||
|
|
||
|
>>> Point = immutable('x, y, id_', name='Point')
|
||
|
>>> p = Point(1, 2, id_=17)
|
||
|
>>> p.set(x=3)
|
||
|
Point(x=3, y=2, id_=17)
|
||
|
>>> p.set(id_=18)
|
||
|
Traceback (most recent call last):
|
||
|
AttributeError: Cannot set frozen members id_
|
||
|
"""
|
||
|
|
||
|
if isinstance(members, str):
|
||
|
members = members.replace(',', ' ').split()
|
||
|
|
||
|
def frozen_member_test():
|
||
|
frozen_members = ["'%s'" % f for f in members if f.endswith('_')]
|
||
|
if frozen_members:
|
||
|
return """
|
||
|
frozen_fields = fields_to_modify & set([{frozen_members}])
|
||
|
if frozen_fields:
|
||
|
raise AttributeError('Cannot set frozen members %s' % ', '.join(frozen_fields))
|
||
|
""".format(frozen_members=', '.join(frozen_members))
|
||
|
|
||
|
return ''
|
||
|
|
||
|
verbose_string = ""
|
||
|
if sys.version_info < (3, 7):
|
||
|
# Verbose is no longer supported in Python 3.7
|
||
|
verbose_string = ", verbose={verbose}".format(verbose=verbose)
|
||
|
|
||
|
quoted_members = ', '.join("'%s'" % m for m in members)
|
||
|
template = """
|
||
|
class {class_name}(namedtuple('ImmutableBase', [{quoted_members}]{verbose_string})):
|
||
|
__slots__ = tuple()
|
||
|
|
||
|
def __repr__(self):
|
||
|
return super({class_name}, self).__repr__().replace('ImmutableBase', self.__class__.__name__)
|
||
|
|
||
|
def set(self, **kwargs):
|
||
|
if not kwargs:
|
||
|
return self
|
||
|
|
||
|
fields_to_modify = set(kwargs.keys())
|
||
|
if not fields_to_modify <= {member_set}:
|
||
|
raise AttributeError("'%s' is not a member" % ', '.join(fields_to_modify - {member_set}))
|
||
|
|
||
|
{frozen_member_test}
|
||
|
|
||
|
return self.__class__.__new__(self.__class__, *map(kwargs.pop, [{quoted_members}], self))
|
||
|
""".format(quoted_members=quoted_members,
|
||
|
member_set="set([%s])" % quoted_members if quoted_members else 'set()',
|
||
|
frozen_member_test=frozen_member_test(),
|
||
|
verbose_string=verbose_string,
|
||
|
class_name=name)
|
||
|
|
||
|
if verbose:
|
||
|
print(template)
|
||
|
|
||
|
from collections import namedtuple
|
||
|
namespace = dict(namedtuple=namedtuple, __name__='pyrsistent_immutable')
|
||
|
try:
|
||
|
exec(template, namespace)
|
||
|
except SyntaxError as e:
|
||
|
raise SyntaxError(str(e) + ':\n' + template) from e
|
||
|
|
||
|
return namespace[name]
|