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:
- Define
to_primitive()
method which accepts an optionalcontext
argument.- Call parent’s method and get a primitive.
- Update that primitive with custom data which may depend on the context.
- 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.