1from pyrsistent._checked_types import (InvariantException, CheckedType, _restore_pickle, store_invariants)
2from pyrsistent._field_common import (
3 set_fields, check_type, is_field_ignore_extra_complaint, PFIELD_NO_INITIAL, serialize, check_global_invariants
4)
5from pyrsistent._transformations import transform
6
7
8def _is_pclass(bases):
9 return len(bases) == 1 and bases[0] == CheckedType
10
11
12class PClassMeta(type):
13 def __new__(mcs, name, bases, dct):
14 set_fields(dct, bases, name='_pclass_fields')
15 store_invariants(dct, bases, '_pclass_invariants', '__invariant__')
16 dct['__slots__'] = ('_pclass_frozen',) + tuple(key for key in dct['_pclass_fields'])
17
18 # There must only be one __weakref__ entry in the inheritance hierarchy,
19 # lets put it on the top level class.
20 if _is_pclass(bases):
21 dct['__slots__'] += ('__weakref__',)
22
23 return super(PClassMeta, mcs).__new__(mcs, name, bases, dct)
24
25_MISSING_VALUE = object()
26
27
28def _check_and_set_attr(cls, field, name, value, result, invariant_errors):
29 check_type(cls, field, name, value)
30 is_ok, error_code = field.invariant(value)
31 if not is_ok:
32 invariant_errors.append(error_code)
33 else:
34 setattr(result, name, value)
35
36
37class PClass(CheckedType, metaclass=PClassMeta):
38 """
39 A PClass is a python class with a fixed set of specified fields. PClasses are declared as python classes inheriting
40 from PClass. It is defined the same way that PRecords are and behaves like a PRecord in all aspects except that it
41 is not a PMap and hence not a collection but rather a plain Python object.
42
43
44 More documentation and examples of PClass usage is available at https://github.com/tobgu/pyrsistent
45 """
46 def __new__(cls, **kwargs): # Support *args?
47 result = super(PClass, cls).__new__(cls)
48 factory_fields = kwargs.pop('_factory_fields', None)
49 ignore_extra = kwargs.pop('ignore_extra', None)
50 missing_fields = []
51 invariant_errors = []
52 for name, field in cls._pclass_fields.items():
53 if name in kwargs:
54 if factory_fields is None or name in factory_fields:
55 if is_field_ignore_extra_complaint(PClass, field, ignore_extra):
56 value = field.factory(kwargs[name], ignore_extra=ignore_extra)
57 else:
58 value = field.factory(kwargs[name])
59 else:
60 value = kwargs[name]
61 _check_and_set_attr(cls, field, name, value, result, invariant_errors)
62 del kwargs[name]
63 elif field.initial is not PFIELD_NO_INITIAL:
64 initial = field.initial() if callable(field.initial) else field.initial
65 _check_and_set_attr(
66 cls, field, name, initial, result, invariant_errors)
67 elif field.mandatory:
68 missing_fields.append('{0}.{1}'.format(cls.__name__, name))
69
70 if invariant_errors or missing_fields:
71 raise InvariantException(tuple(invariant_errors), tuple(missing_fields), 'Field invariant failed')
72
73 if kwargs:
74 raise AttributeError("'{0}' are not among the specified fields for {1}".format(
75 ', '.join(kwargs), cls.__name__))
76
77 check_global_invariants(result, cls._pclass_invariants)
78
79 result._pclass_frozen = True
80 return result
81
82 def set(self, *args, **kwargs):
83 """
84 Set a field in the instance. Returns a new instance with the updated value. The original instance remains
85 unmodified. Accepts key-value pairs or single string representing the field name and a value.
86
87 >>> from pyrsistent import PClass, field
88 >>> class AClass(PClass):
89 ... x = field()
90 ...
91 >>> a = AClass(x=1)
92 >>> a2 = a.set(x=2)
93 >>> a3 = a.set('x', 3)
94 >>> a
95 AClass(x=1)
96 >>> a2
97 AClass(x=2)
98 >>> a3
99 AClass(x=3)
100 """
101 if args:
102 kwargs[args[0]] = args[1]
103
104 factory_fields = set(kwargs)
105
106 for key in self._pclass_fields:
107 if key not in kwargs:
108 value = getattr(self, key, _MISSING_VALUE)
109 if value is not _MISSING_VALUE:
110 kwargs[key] = value
111
112 return self.__class__(_factory_fields=factory_fields, **kwargs)
113
114 @classmethod
115 def create(cls, kwargs, _factory_fields=None, ignore_extra=False):
116 """
117 Factory method. Will create a new PClass of the current type and assign the values
118 specified in kwargs.
119
120 :param ignore_extra: A boolean which when set to True will ignore any keys which appear in kwargs that are not
121 in the set of fields on the PClass.
122 """
123 if isinstance(kwargs, cls):
124 return kwargs
125
126 if ignore_extra:
127 kwargs = {k: kwargs[k] for k in cls._pclass_fields if k in kwargs}
128
129 return cls(_factory_fields=_factory_fields, ignore_extra=ignore_extra, **kwargs)
130
131 def serialize(self, format=None):
132 """
133 Serialize the current PClass using custom serializer functions for fields where
134 such have been supplied.
135 """
136 result = {}
137 for name in self._pclass_fields:
138 value = getattr(self, name, _MISSING_VALUE)
139 if value is not _MISSING_VALUE:
140 result[name] = serialize(self._pclass_fields[name].serializer, format, value)
141
142 return result
143
144 def transform(self, *transformations):
145 """
146 Apply transformations to the currency PClass. For more details on transformations see
147 the documentation for PMap. Transformations on PClasses do not support key matching
148 since the PClass is not a collection. Apart from that the transformations available
149 for other persistent types work as expected.
150 """
151 return transform(self, transformations)
152
153 def __eq__(self, other):
154 if isinstance(other, self.__class__):
155 for name in self._pclass_fields:
156 if getattr(self, name, _MISSING_VALUE) != getattr(other, name, _MISSING_VALUE):
157 return False
158
159 return True
160
161 return NotImplemented
162
163 def __ne__(self, other):
164 return not self == other
165
166 def __hash__(self):
167 # May want to optimize this by caching the hash somehow
168 return hash(tuple((key, getattr(self, key, _MISSING_VALUE)) for key in self._pclass_fields))
169
170 def __setattr__(self, key, value):
171 if getattr(self, '_pclass_frozen', False):
172 raise AttributeError("Can't set attribute, key={0}, value={1}".format(key, value))
173
174 super(PClass, self).__setattr__(key, value)
175
176 def __delattr__(self, key):
177 raise AttributeError("Can't delete attribute, key={0}, use remove()".format(key))
178
179 def _to_dict(self):
180 result = {}
181 for key in self._pclass_fields:
182 value = getattr(self, key, _MISSING_VALUE)
183 if value is not _MISSING_VALUE:
184 result[key] = value
185
186 return result
187
188 def __repr__(self):
189 return "{0}({1})".format(self.__class__.__name__,
190 ', '.join('{0}={1}'.format(k, repr(v)) for k, v in self._to_dict().items()))
191
192 def __reduce__(self):
193 # Pickling support
194 data = dict((key, getattr(self, key)) for key in self._pclass_fields if hasattr(self, key))
195 return _restore_pickle, (self.__class__, data,)
196
197 def evolver(self):
198 """
199 Returns an evolver for this object.
200 """
201 return _PClassEvolver(self, self._to_dict())
202
203 def remove(self, name):
204 """
205 Remove attribute given by name from the current instance. Raises AttributeError if the
206 attribute doesn't exist.
207 """
208 evolver = self.evolver()
209 del evolver[name]
210 return evolver.persistent()
211
212
213class _PClassEvolver(object):
214 __slots__ = ('_pclass_evolver_original', '_pclass_evolver_data', '_pclass_evolver_data_is_dirty', '_factory_fields')
215
216 def __init__(self, original, initial_dict):
217 self._pclass_evolver_original = original
218 self._pclass_evolver_data = initial_dict
219 self._pclass_evolver_data_is_dirty = False
220 self._factory_fields = set()
221
222 def __getitem__(self, item):
223 return self._pclass_evolver_data[item]
224
225 def set(self, key, value):
226 if self._pclass_evolver_data.get(key, _MISSING_VALUE) is not value:
227 self._pclass_evolver_data[key] = value
228 self._factory_fields.add(key)
229 self._pclass_evolver_data_is_dirty = True
230
231 return self
232
233 def __setitem__(self, key, value):
234 self.set(key, value)
235
236 def remove(self, item):
237 if item in self._pclass_evolver_data:
238 del self._pclass_evolver_data[item]
239 self._factory_fields.discard(item)
240 self._pclass_evolver_data_is_dirty = True
241 return self
242
243 raise AttributeError(item)
244
245 def __delitem__(self, item):
246 self.remove(item)
247
248 def persistent(self):
249 if self._pclass_evolver_data_is_dirty:
250 return self._pclass_evolver_original.__class__(_factory_fields=self._factory_fields,
251 **self._pclass_evolver_data)
252
253 return self._pclass_evolver_original
254
255 def __setattr__(self, key, value):
256 if key not in self.__slots__:
257 self.set(key, value)
258 else:
259 super(_PClassEvolver, self).__setattr__(key, value)
260
261 def __getattr__(self, item):
262 return self[item]