candv.core   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 580
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 174
dl 0
loc 580
rs 8.5599
c 0
b 0
f 0
wmc 48

35 Methods

Rating   Name   Duplication   Size   Complexity  
A SimpleConstant.__init__() 0 3 1
A SimpleConstant._post_init() 0 7 1
A SimpleConstant.full_name() 0 8 2
A SimpleConstant.to_group() 0 34 1
A SimpleConstant.merge_into_group() 0 2 1
A Constants.__new__() 0 3 1
A _LazyConstantsGroup._make_to_primitive() 0 12 1
A _ConstantsContainerMeta._make_members_from_attributes() 0 21 5
A _ConstantsContainerMeta.iternames() 0 8 1
A SimpleConstant.__eq__() 0 7 1
A _ConstantsContainerMeta.items() 0 27 1
A _LazyConstantsGroup._validate_group_members() 0 6 3
A SimpleConstant.__ne__() 0 5 1
A _ConstantsContainerMeta._ensure_attribute() 0 4 2
A _ConstantsContainerMeta.constants() 0 26 1
A _ConstantsContainerMeta.get() 0 39 1
A _ConstantsContainerMeta._get_or_make_repr_value() 0 9 2
A _LazyConstantsGroup._evaluate() 0 19 1
A _ConstantsContainerMeta.to_primitive() 0 15 1
A _ConstantsContainerMeta._validate_constant_class() 0 7 2
A _ConstantsContainerMeta.iteritems() 0 8 1
A _ConstantsContainerMeta.has_name() 0 11 1
A SimpleConstant.to_primitive() 0 12 1
A _ConstantsContainerMeta.__iter__() 0 2 1
A _ConstantsContainerMeta.__getitem__() 0 35 2
A SimpleConstant.__repr__() 0 6 1
A _LazyConstantsGroup.__init__() 0 5 1
A _ConstantsContainerMeta.__repr__() 0 2 1
A _ConstantsContainerMeta.names() 0 26 1
A _ConstantsContainerMeta.__contains__() 0 2 1
A _ConstantsContainerMeta.iterconstants() 0 8 1
A _ConstantsContainerMeta.__new__() 0 14 1
A _ConstantsContainerMeta.__len__() 0 2 1
A _ConstantsContainerMeta._validate_constant_is_not_bound() 0 5 2
A SimpleConstant.__hash__() 0 5 1

1 Function

Rating   Name   Duplication   Size   Complexity  
A with_constant_class() 0 32 1

How to fix   Complexity   

Complexity

Complex classes like candv.core often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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