123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- import sys
- import six
- 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, six.string_types):
- 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:
- six.exec_(template, namespace)
- except SyntaxError as e:
- raise SyntaxError(e.message + ':\n' + template)
- return namespace[name]
|