4. Customization

It is possible to create custom classes of constant and of containers if standard functionality is not enough.

4.1. Custom definitions

There are several reasons why one would need to create a custom class of constants. For example:

  • A need to vividly define a type of constants tracked by a certain container.
  • A need to add extra methods to constants.
  • A need to add extra attributes to constants.

Custom constants can be created simply by subclassing one of existing classes of constants, e.g.:

1
2
3
4
from candv import SimpleConstant

class SupportedLanguage(SimpleConstant):
  ...

Here, SupportedLanguage is quite ready to be used, e.g.:

 5
 6
 7
 8
 9
10
from candv import Constants


class SupportedLanguages(Constants):
  en = SupportedLanguage()
  fr = SupportedLanguage()

Despite SupportedLanguages is a valid container, it does not enforce which constants are its valid members. For example, it’s still possible to use other constants:

11
12
13
14
15
class SupportedLanguages(Constants):
  en = SupportedLanguage()
  fr = SupportedLanguage()

  xx = SimpleConstant()

Here, all constants will be visible to the container:

16
17
>>> SupportedLanguages.names()
['en', 'fr', 'xx']

If a container has methods relying on custom attributes of its members, such behavior might become troublesome.

One should specify constant_class attribute in order to explicitly define constants supported by a container. So, a bit more correct definition would be:

18
19
20
21
22
class SupportedLanguages(Constants):
  constant_class = SupportedLanguage

  en = SupportedLanguage()
  fr = SupportedLanguage()

As a result, any constants except SupportedLanguage and its derivatives will be ignored:

23
24
25
26
27
28
29
class SupportedLanguages(Constants):
  constant_class = SupportedLanguage

  en = SupportedLanguage()
  fr = SupportedLanguage()

  xx = SimpleConstant()
30
31
>>> SupportedLanguages.names()
['en', 'fr']

As definitions of the constant_class attribute may clutter definitions of classes, it’s possible to lift them out of class bodies using a helper with_constant_class():

32
33
34
35
36
37
from candv import with_constant_class


class SupportedLanguages(with_constant_class(SupportedLanguage), Constants):
  en = SupportedLanguage()
  fr = SupportedLanguage()

Of course, it’s possible to add custom methods and attributes to both constants and containers.

For example, the following constants allow formatting and parsing of operations having opcodes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from candv import ValueConstant
from candv import Values
from candv import with_constant_class


class Opcode(ValueConstant):

  def compose(self, *args):
    chunks = [self.value, ]
    chunks.extend(args)
    return '/'.join(map(str, chunks))


class OPERATIONS(with_constant_class(Opcode), Values):
  REQ = Opcode(100)
  ACK = Opcode(200)

  @classmethod
  def decompose(cls, value):
    chunks = value.split('/')
    opcode = int(chunks.pop(0))
    constant = cls.get_by_value(opcode)
    return constant, chunks

Example usage of such constants is defined as follows.

24
25
26
27
28
>>> OPERATIONS.ACK.compose(1, 2, 'foo')
'200/1/2/foo'

>>> OPERATIONS.decompose('200/1/2/foo')
(<constant 'OPERATIONS.ACK'>, ['1', '2', 'foo'])

The point here is to show that it is possible to add arbitrary attributes and logic to constants if really needed.

4.2. Adding verbosity

If custom constants need to have human-friendly attributes provided by VerboseConstant, they can be added by VerboseMixin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from candv import SimpleConstant
from candv import VerboseMixin


class CustomConstant(VerboseMixin, SimpleConstant):

    def __init__(self, arg1, agr2, verbose_name=None, help_text=None):
      super().__init__(
        verbose_name=verbose_name,
        help_text=help_text,
      )
      self.arg1 = arg1
      self.arg2 = arg2

Note

Here, verbose_name and help_text attributes must be passed as keyword arguments during super().__init__() call.

4.3. Custom conversion to primitives

Custom constants which have complex attributes may need to define custom logic for converting their attributes into primitives. This is primarily needed for serialization, say, into JSON.

One has to override to_primitive() method to define custom conversion logic. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from fractions import Fraction
from pprint import pprint

from candv import Constants
from candv import SimpleConstant
from candv import with_constant_class


class FractionConstant(SimpleConstant):

  def __init__(self, value):
    super().__init__()
    self.value = value

  def to_primitive(self, context=None):
    primitive = super().to_primitive(context)
    primitive.update({
      'numerator':   self.value.numerator,
      'denominator': self.value.denominator
    })
    return primitive


class Fractions(with_constant_class(FractionConstant), Constants):
  one_half  = FractionConstant(Fraction(1, 2))
  one_third = FractionConstant(Fraction(1, 3))
26
27
28
29
30
31
32
>>> Fractions.one_half.to_primitive()
{'name': 'one_half', 'numerator': 1, 'denominator': 2}

>>> pprint(Fractions.to_primitive())
{'items': [{'denominator': 2, 'name': 'one_half', 'numerator': 1},
           {'denominator': 3, 'name': 'one_third', 'numerator': 1}],
 'name': 'Fractions'}

The plot in a nutshell:

  1. Define to_primitive() method which accepts an optional context argument.
  2. Call parent’s method and get a primitive.
  3. Update that primitive with custom data which may depend on the context.
  4. Return the updated primitive.

The same can be applied to custom constant containers as well.

4.4. Hierarchies

Hierarchies are made by creating groups from constants objects. Since groups are created dynamically, original attributes and methods of constants have to be supplied to groups.

This can be done by overriding merge_into_group() method. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from candv import Values
from candv import ValueConstant


class Opcode(ValueConstant):

  # custom method that also needs to be available in groups
  def compose(self, *args):
    chunks = [self.value, ]
    chunks.extend(args)
    return '/'.join(map(str, chunks))

  def merge_into_group(self, group):
    super().merge_into_group(group)
    group.compose = self.compose
16
17
18
19
class FOO(Values):
  BAR = Opcode(300).to_group(Values,
    BAZ = Opcode(301),
  )
20
21
22
23
24
>>> FOO.BAR.compose(1, 2, 3)
'300/1/2/3'

>>> FOO.BAR.BAZ.compose(5, 6)
'301/5/6'

Here, the overridden method merge_into_group() calls the original method of the base class and adds a new compose attribute to the group.

In this simple case the attribute is a reference to the compose() method of the custom Opcode class.

Warning

Attaching methods of existing objects to another objects can be not a good idea.

Consider using method factories or at least lambdas.