Passed
Branch master (9e8f7a)
by Oleksandr
01:29
created

candv.core.SimpleConstant.__init__()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
"""
2
Defines base constant and base container for constants.
3
4
"""
5
import types
6
7
from collections import OrderedDict as odict
8
9
from .exceptions import CandvConstantAlreadyBoundError
10
from .exceptions import CandvContainerMisusedError
11
from .exceptions import CandvInvalidConstantClass
12
from .exceptions import CandvInvalidGroupMemberError
13
from .exceptions import CandvMissingConstantError
14
15
from .utils import export
16
17
18
UNBOUND_CONSTANT_CONTAINER_NAME = "__UNBOUND__"
19
20
21
@export
22
class SimpleConstant:
23
  """
24
  Base class for all constants.
25
26
  :ivar str name:
27
    constant's name: set up automatically and is equal to the name of the
28
    container's attribute
29
30
  """
31
32
  def __init__(self):
33
    self.name = None
34
    self.container = None
35
36
  def _post_init(self, name, container=None):
37
    """
38
    Called automatically by the container after container's class construction.
39
40
    """
41
    self.name = name
42
    self.container = container
43
44
  def to_group(self, group_class, **group_members):
45
    """
46
    Convert a constant into a constants group.
47
48
    :param class group_class:
49
      a class of group container which will be created
50
51
    :param group_members:
52
      unpacked dict which defines group members
53
54
    :returns:
55
      a lazy constants group which will be evaluated by the container.
56
      Method :meth:`merge_into_group` will be called during evaluation of the
57
      group
58
59
    **Example**:
60
61
    .. code-block:: python
62
63
      from candv import Constants
64
      from candv import SimpleConstant
65
66
67
      class FOO(Constants):
68
        A = SimpleConstant()
69
        B = SimpleConstant().to_group(Constants,
70
71
          B2 = SimpleConstant(),
72
          B0 = SimpleConstant(),
73
          B1 = SimpleConstant(),
74
        )
75
76
    """
77
    return _LazyConstantsGroup(self, group_class, **group_members)
78
79
  def merge_into_group(self, group):
80
    """
81
    Called automatically by the container after group construction.
82
83
    .. note::
84
85
      Redefine this method in all derived classes. Attach all custom attributes
86
      and methods to the group here.
87
88
    :param group:
89
      an instance of :class:`Constants` or of its subclass into which
90
      this constant will be merged
91
92
    :returns: ``None``
93
94
    """
95
96
  @property
97
  def full_name(self):
98
    prefix = (
99
      self.container.full_name
100
      if self.container
101
      else UNBOUND_CONSTANT_CONTAINER_NAME
102
    )
103
    return f"{prefix}.{self.name}"
104
105
  def to_primitive(self, context=None):
106
    """
107
    Represent the constant via Python's primitive data structures.
108
109
    .. versionadded:: 1.3.0
110
111
    """
112
    return {
113
      'name': self.name,
114
    }
115
116
  def __repr__(self):
117
    """
118
    Produce a text identifying the constant.
119
120
    """
121
    return f"<constant '{self.full_name}'>"
122
123
  def __hash__(self):
124
    """
125
    .. versionadded:: 1.3.1
126
    """
127
    return hash(self.full_name)
128
129
  def __eq__(self, other):
130
    """
131
    .. versionadded:: 1.3.1
132
    """
133
    return (
134
      isinstance(other, SimpleConstant)
135
      and (self.full_name == other.full_name)
136
    )
137
138
  def __ne__(self, other):
139
    """
140
    .. versionadded:: 1.3.1
141
    """
142
    return not (self == other)
143
144
145
class _LazyConstantsGroup:
146
147
  def __init__(self, constant, group_class, **group_members):
148
    self._validate_group_members(group_members)
149
    self.constant = constant
150
    self.group_class = group_class
151
    self.group_members = group_members
152
153
  @staticmethod
154
  def _validate_group_members(group_members):
155
    for name, obj in group_members.items():
156
      if not isinstance(obj, (SimpleConstant, _LazyConstantsGroup)):
157
        raise CandvInvalidGroupMemberError(
158
          f'invalid group member "{obj}": only instances of "{SimpleConstant}" '
159
          f'or other groups are allowed'
160
        )
161
162
  def _evaluate(self, parent, name):
163
    full_name = f"{parent.full_name}.{name}"
164
    group_bases = (self.group_class, )
165
    self.group_members.update({
166
      'name': name,
167
      'full_name': full_name,
168
      'container': parent,
169
      '__repr': f"<constants group '{full_name}'>",
170
    })
171
172
    group = type(full_name, group_bases, self.group_members)
173
    group.to_primitive = self._make_to_primitive(group, self.constant)
174
    self.constant.merge_into_group(group)
175
176
    del self.constant
177
    del self.group_class
178
    del self.group_members
179
180
    return group
181
182
  @staticmethod
183
  def _make_to_primitive(group, constant):
184
    # define aliases to avoid shadowing and infinite recursion
185
    constant_primitive = constant.to_primitive
186
    group_primitive = group.to_primitive
187
188
    def to_primitive(self, context=None):
189
      primitive = constant_primitive(context)
190
      primitive.update(group_primitive(context))
191
      return primitive
192
193
    return types.MethodType(to_primitive, group)
194
195
196
class _ConstantsContainerMeta(type):
197
  """
198
  Metaclass for creating container classes for constants.
199
200
  """
201
  def __new__(self, class_name, bases, attributes):
202
    self._ensure_attribute(attributes, "name", class_name)
203
    self._ensure_attribute(attributes, "full_name", class_name)
204
205
    cls = super().__new__(self, class_name, bases, attributes)
206
207
    # set before validations to get correct repr
208
    cls.__repr = self._get_or_make_repr_value(attributes)
209
210
    self._validate_constant_class(cls)
211
212
    cls._members = self._make_members_from_attributes(cls, attributes)
213
214
    return cls
215
216
  @staticmethod
217
  def _ensure_attribute(attributes, attribute_name, default_value):
218
    if attribute_name not in attributes:
219
      attributes[attribute_name] = default_value
220
221
  @staticmethod
222
  def _validate_constant_class(target_cls):
223
    constant_class = getattr(target_cls, "constant_class", None)
224
225
    if not issubclass(constant_class, SimpleConstant):
226
      raise CandvInvalidConstantClass(
227
        f'invalid "constant_class" for "{target_cls}": must be derived from '
228
        f'"{SimpleConstant}", but got "{constant_class}"'
229
      )
230
231
  @staticmethod
232
  def _get_or_make_repr_value(attributes):
233
    value = attributes.pop("__repr", None)
234
235
    if not value:
236
      name = attributes["name"]
237
      value = f"<constants container '{name}'>"
238
239
    return value
240
241
  @classmethod
242
  def _make_members_from_attributes(cls, target_cls, attributes):
243
    members = []
244
245
    for name, the_object in attributes.items():
246
      if isinstance(the_object, _LazyConstantsGroup):
247
        group = the_object._evaluate(target_cls, name)
248
        setattr(target_cls, name, group)
249
        members.append((name, group))
250
251
      elif isinstance(the_object, target_cls.constant_class):
252
        cls._validate_constant_is_not_bound(target_cls, name, the_object)
253
        the_object._post_init(name=name, container=target_cls)
254
        members.append((name, the_object))
255
256
      elif isinstance(the_object, SimpleConstant):
257
        # init but do not append constants which are more generic
258
        # than ``constant_class``
259
        the_object._post_init(name=name)
260
261
    return odict(members)
262
263
  @staticmethod
264
  def _validate_constant_is_not_bound(target_cls, attribute_name, the_object):
265
    if the_object.container is not None:
266
      raise CandvConstantAlreadyBoundError(
267
        f'cannot use "{the_object}" as value for "{attribute_name}" attribute '
268
        f'of "{target_cls}" container: already bound to "{the_object.container}"'
269
      )
270
271
  def __repr__(self):
272
    return self.__repr
273
274
  def __getitem__(self, name):
275
    """
276
    Try to get constant by its name.
277
278
    :param str name: name of constant to search for
279
280
    :returns: a constant
281
    :rtype: an instance of :class:`SimpleConstant` or its subclass
282
283
    :raises CandvMissingConstantError:
284
      if constant with name ``name`` is not present in container
285
286
    **Example**:
287
288
    .. code-block:: python
289
290
      from candv import Constants
291
      from candv import SimpleConstant
292
293
294
      class FOO(Constants):
295
        foo = SimpleConstant()
296
        bar = SimpleConstant()
297
298
    .. code-block:: python
299
300
      >>> FOO['foo']
301
      <constant 'FOO.foo'>
302
303
    """
304
    try:
305
      return self._members[name]
306
    except KeyError:
307
      raise CandvMissingConstantError(
308
        f'constant "{name}" is not present in "{self}"'
309
      )
310
311
  def __contains__(self, name):
312
    return name in self._members
313
314
  def __len__(self):
315
    return len(self._members)
316
317
  def __iter__(self):
318
    return self.iternames()
319
320
  def get(self, name, default=None):
321
    """
322
    Try to get a constant by its name or fallback to a default.
323
324
    :param str name:
325
      name of constant to search
326
327
    :param default:
328
      an object returned by default if constant with a given name is not present
329
      in the container
330
331
    :returns: a constant or a default value
332
    :rtype: an instance of :class:`SimpleConstant` or its subclass, or `default` value
333
334
    **Example**:
335
336
    .. code-block:: python
337
338
      from candv import Constants
339
      from candv import SimpleConstant
340
341
342
      class FOO(Constants):
343
        foo = SimpleConstant()
344
        bar = SimpleConstant()
345
346
    .. code-block:: python
347
348
      >>> FOO.get('foo')
349
      <constant 'FOO.foo'>
350
351
      >>> FOO.get('xxx')
352
      None
353
354
      >>> FOO.get('xxx', default=123)
355
      123
356
357
    """
358
    return self._members.get(name, default)
359
360
  def has_name(self, name):
361
    """
362
    Check if the container has a constant with a given name.
363
364
    :param str name: a constant's name to check
365
366
    :returns: ``True`` if given name belongs to container, ``False`` otherwise
367
    :rtype: :class:`bool`
368
369
    """
370
    return name in self
371
372
  def names(self):
373
    """
374
    List all names of constants within the container.
375
376
    :returns: a list of constant names in order constants were defined
377
    :rtype: :class:`list` of strings
378
379
    **Example**:
380
381
    .. code-block:: python
382
383
      from candv import Constants
384
      from candv import SimpleConstant
385
386
387
      class FOO(Constants):
388
        foo = SimpleConstant()
389
        bar = SimpleConstant()
390
391
    .. code-block:: python
392
393
      >>> FOO.names()
394
      ['foo', 'bar']
395
396
    """
397
    return list(self._members.keys())
398
399
  def iternames(self):
400
    """
401
    Get an iterator over constants names.
402
403
    Same as :meth:`names`, but returns an interator.
404
405
    """
406
    return iter(self._members.keys())
407
408
  def constants(self):
409
    """
410
    List all constants in the container.
411
412
    :returns: list of constants in order they were defined
413
    :rtype: :class:`list`
414
415
    **Example**:
416
417
    .. code-block:: python
418
419
      from candv import Constants
420
      from candv import SimpleConstant
421
422
423
      class FOO(Constants):
424
        foo = SimpleConstant()
425
        bar = SimpleConstant()
426
427
    .. code-block:: python
428
429
      >>> [x.name for x in FOO.constants()]
430
      ['foo', 'bar']
431
432
    """
433
    return list(self._members.values())
434
435
  def iterconstants(self):
436
    """
437
    Get an iterator over constants.
438
439
    Same as :meth:`constants`, but returns an interator
440
441
    """
442
    return iter(self._members.values())
443
444
  def items(self):
445
    """
446
    Get list of constants names along with constants themselves.
447
448
    :returns:
449
      list of constants with their names in order they were defined.
450
      Each element in the list is a :class:`tuple` in format ``(name, constant)``.
451
    :rtype: :class:`list`
452
453
    **Example**:
454
455
    .. code-block:: python
456
457
      from candv import Constants
458
      from candv import SimpleConstant
459
460
      class FOO(Constants):
461
        foo = SimpleConstant()
462
        bar = SimpleConstant()
463
464
    .. code-block:: python
465
466
      >>> FOO.items()
467
      [('foo', <constant 'FOO.foo'>), ('bar', <constant 'FOO.bar'>)]
468
469
    """
470
    return list(self._members.items())
471
472
  def iteritems(self):
473
    """
474
    Get an iterator over constants names along with constants themselves.
475
476
    Same as :meth:`items`, but returns an interator
477
478
    """
479
    return iter(self._members.items())
480
481
  #: .. versionadded:: 1.1.2
482
  #:
483
  #: Alias for :meth:`constants`.
484
  #: Added for consistency with dictionaries. Use :class:`~candv.Values` and
485
  #: :meth:`~candv.Values.values` if you need to have constants with real
486
  #: values.
487
  values = constants
488
489
  #: .. versionadded:: 1.1.2
490
  #:
491
  #: Alias for :meth:`iterconstants`.
492
  #: Added for consistency with dictionaries. Use :class:`~candv.Values` and
493
  #: :meth:`~candv.Values.itervalues` if you need to have constants with real
494
  #: values.
495
  itervalues = iterconstants
496
497
  def to_primitive(self, context=None):
498
    """
499
    .. versionadded:: 1.3.0
500
    """
501
    items = [
502
      x.to_primitive(context)
503
      for x in self.iterconstants()
504
    ]
505
    return {
506
      'name': self.name,
507
      'items': items,
508
    }
509
510
511
@export
512
class Constants(metaclass=_ConstantsContainerMeta):
513
  """
514
  Base class for creating constants containers.
515
516
  Each constant defined within the container will remember its creation order.
517
518
  See an example in :meth:`constants`.
519
520
  :cvar constant_class:
521
    defines a class of constants which a container will store.
522
    This attribute **MUST** be set up manually when you define a new container
523
    type. Otherwise container will not be initialized. Default: ``None``
524
525
  :raises CandvContainerMisusedError:
526
    if you try to create an instance of container. Containers are singletons and
527
    they cannot be instantiated. Their attributes must be used directly.
528
529
  """
530
531
  #: Defines a top-level class of constants which can be stored by container
532
  constant_class = SimpleConstant
533
534
  def __new__(cls):
535
    raise CandvContainerMisusedError(
536
      f'"{cls}" cannot be instantiated: constant containers are not designed '
537
      f'for that'
538
    )
539
540
541
@export
542
def with_constant_class(the_class):
543
  """
544
  Create a mixin class with ``constant_class`` attribute.
545
546
  Allows to set a constant class for constants container outside container itself.
547
  This may help to create more readable container definition, e.g.:
548
549
  .. code-block:: python
550
551
    from candv import Constants
552
    from candv import SimpleConstant
553
    from candv import with_constant_class
554
555
556
    class CustomConstant(SimpleConstant):
557
      ...
558
559
    class FOO(with_constant_class(CustomConstant), Constants):
560
      A = CustomConstant()
561
      B = CustomConstant()
562
563
  .. code-block:: python
564
565
    >>> FOO.constant_class
566
    <class '__main__.CustomConstant'>
567
568
  """
569
  class ConstantsMixin:
570
    constant_class = the_class
571
572
  return ConstantsMixin
573