_immutable.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import sys
  2. import six
  3. def immutable(members='', name='Immutable', verbose=False):
  4. """
  5. Produces a class that either can be used standalone or as a base class for persistent classes.
  6. This is a thin wrapper around a named tuple.
  7. Constructing a type and using it to instantiate objects:
  8. >>> Point = immutable('x, y', name='Point')
  9. >>> p = Point(1, 2)
  10. >>> p2 = p.set(x=3)
  11. >>> p
  12. Point(x=1, y=2)
  13. >>> p2
  14. Point(x=3, y=2)
  15. Inheriting from a constructed type. In this case no type name needs to be supplied:
  16. >>> class PositivePoint(immutable('x, y')):
  17. ... __slots__ = tuple()
  18. ... def __new__(cls, x, y):
  19. ... if x > 0 and y > 0:
  20. ... return super(PositivePoint, cls).__new__(cls, x, y)
  21. ... raise Exception('Coordinates must be positive!')
  22. ...
  23. >>> p = PositivePoint(1, 2)
  24. >>> p.set(x=3)
  25. PositivePoint(x=3, y=2)
  26. >>> p.set(y=-3)
  27. Traceback (most recent call last):
  28. Exception: Coordinates must be positive!
  29. The persistent class also supports the notion of frozen members. The value of a frozen member
  30. cannot be updated. For example it could be used to implement an ID that should remain the same
  31. over time. A frozen member is denoted by a trailing underscore.
  32. >>> Point = immutable('x, y, id_', name='Point')
  33. >>> p = Point(1, 2, id_=17)
  34. >>> p.set(x=3)
  35. Point(x=3, y=2, id_=17)
  36. >>> p.set(id_=18)
  37. Traceback (most recent call last):
  38. AttributeError: Cannot set frozen members id_
  39. """
  40. if isinstance(members, six.string_types):
  41. members = members.replace(',', ' ').split()
  42. def frozen_member_test():
  43. frozen_members = ["'%s'" % f for f in members if f.endswith('_')]
  44. if frozen_members:
  45. return """
  46. frozen_fields = fields_to_modify & set([{frozen_members}])
  47. if frozen_fields:
  48. raise AttributeError('Cannot set frozen members %s' % ', '.join(frozen_fields))
  49. """.format(frozen_members=', '.join(frozen_members))
  50. return ''
  51. verbose_string = ""
  52. if sys.version_info < (3, 7):
  53. # Verbose is no longer supported in Python 3.7
  54. verbose_string = ", verbose={verbose}".format(verbose=verbose)
  55. quoted_members = ', '.join("'%s'" % m for m in members)
  56. template = """
  57. class {class_name}(namedtuple('ImmutableBase', [{quoted_members}]{verbose_string})):
  58. __slots__ = tuple()
  59. def __repr__(self):
  60. return super({class_name}, self).__repr__().replace('ImmutableBase', self.__class__.__name__)
  61. def set(self, **kwargs):
  62. if not kwargs:
  63. return self
  64. fields_to_modify = set(kwargs.keys())
  65. if not fields_to_modify <= {member_set}:
  66. raise AttributeError("'%s' is not a member" % ', '.join(fields_to_modify - {member_set}))
  67. {frozen_member_test}
  68. return self.__class__.__new__(self.__class__, *map(kwargs.pop, [{quoted_members}], self))
  69. """.format(quoted_members=quoted_members,
  70. member_set="set([%s])" % quoted_members if quoted_members else 'set()',
  71. frozen_member_test=frozen_member_test(),
  72. verbose_string=verbose_string,
  73. class_name=name)
  74. if verbose:
  75. print(template)
  76. from collections import namedtuple
  77. namespace = dict(namedtuple=namedtuple, __name__='pyrsistent_immutable')
  78. try:
  79. six.exec_(template, namespace)
  80. except SyntaxError as e:
  81. raise SyntaxError(e.message + ':\n' + template)
  82. return namespace[name]