3. Usage

The concept behind candv is that constants are grouped together into containers.

Those containers are special classes having constants as class attributes. All containers are created by subclassing Constants. Containers cannot be instantiated.

In their turn, constants are special objects, which are instances of SimpleConstant or of its derivatives.

As containers are classes and constants are instances of classes, they can and actually have own methods and attributes.

It is possible to add custom functionality simply by subclassing Constants and SimpleConstant respectively.

In addition to the basic constants and basic containers, candv also provides extended ones like VerboseConstant, ValueConstant, Values, etc.

3.1. Simple constants

Simple constants are really simple. They do not have any particular values attached and resemble enum.Enum used with enum.auto:

1
2
3
4
5
6
7
from candv import Constants
from candv import SimpleConstant


class STATUS(Constants):
  SUCCESS = SimpleConstant()
  FAILURE = SimpleConstant()

Here, STATUS is a subclass of Constants. It acts as a container:

8
9
>>> STATUS
<constants container 'STATUS'>

All containers have the following attributes:

  • name
  • full_name

By default they are equal to the name of the class itself:

10
11
12
13
14
>>> STATUS.name
'STATUS'

>>> STATUS.full_name
'STATUS'

Note

If there is a reason on the Earth to define custom names, it can be done:

class STATUS(Constants):
  name = "foo"
  full_name = f"package.{name}"

  SUCCESS = SimpleConstant()
  FAILURE = SimpleConstant()

The same attributes are available to all constants as well:

15
16
17
18
19
>>> STATUS.SUCCESS.name
'SUCCESS'

>>> STATUS.SUCCESS.full_name
'STATUS.SUCCESS'

As can be seen from the above, names of constants are equal to the names of container’s attributes. And full names combine names of constants with full names of their containers. Custom values are not allowed.

Next, all containers have a member-access API similar to the API of Python’s dict:

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
>>> STATUS.names()
['SUCCESS', 'FAILURE']

>>> STATUS.iternames()
<odict_iterator object at 0x7f289fa6e680>

>>> STATUS.constants()
[<constant 'STATUS.SUCCESS'>, <constant 'STATUS.FAILURE'>]

>>> STATUS.iterconstants()
<odict_iterator object at 0x7f289fa6ecc0>

>>> STATUS.items()
[('SUCCESS', <constant 'STATUS.SUCCESS'>), ('FAILURE', <constant 'STATUS.FAILURE'>)]

>>> STATUS.iteritems()
<odict_iterator object at 0x7f289fa1e360>

>>> list(STATUS)
['SUCCESS', 'FAILURE']

>>> len(STATUS)
2

>>> STATUS['SUCCESS']
<constant 'STATUS.SUCCESS'>

>>> 'SUCCESS' in STATUS
True

>>> STATUS.has_name('PENDING')
False

>>> STATUS.get('XXX')
None

>>> STATUS.get('XXX', default=999)
999

Note

Since 1.1.2 it is possible to list constants and get the same result by calling values() and itervalues():

>>> STATUS.values()
[<constant 'STATUS.SUCCESS'>, <constant 'STATUS.FAILURE'>]

>>> STATUS.itervalues()
<odict_iterator object at 0x7f289fa17b30>

These methods are overridden in Values (see the section below).

In addition to the item-access, containers also provide a dot-access for their constants:

58
59
>>> STATUS.SUCCESS
<constant 'STATUS.SUCCESS'>

Finally, every constant has access to own containers via the container attribute:

60
61
>>> STATUS.SUCCESS.container
<constants container 'STATUS'>

3.2. Constants with values

Constants with values are created via ValueConstant and can have arbitrary values attached to them.

Such constants have to be contained by derivatives of Values class. This enables additional functionality like inverse lookups, i.e. lookups of constants by their values.

1
2
3
4
5
6
7
8
from candv import ValueConstant
from candv import Values


class TEAMS(Values):
  NONE = ValueConstant('#EEE')
  RED  = ValueConstant('#F00')
  BLUE = ValueConstant('#00F')

Here, TEAMS is a subclass of Values, which is a specialized version of Constants. And ValueConstant is a specialized version of SimpleConstant:

 9
10
11
12
13
>>> Values.mro()
[<constants container 'Values'>, <constants container 'Constants'>, <class 'object'>]

>>> ValueConstant.mro()
[<class 'candv.ext.ValueConstant'>, <class 'candv.core.SimpleConstant'>, <class 'object'>]

So, TEAMS has all of the attributes and methods described above. Its values() method returns actual values of its constants:

14
15
16
17
18
>>> TEAMS.values()
['#EEE', '#F00', '#00F']

>>> TEAMS.itervalues()
<map object at 0x7f289fa54ac0>

Values of constants themselves are also accessible:

19
20
>>> TEAMS.RED.value
'#F00'

In addition to the previously mentioned get() method, Values provides get_by_value() method:

21
22
>>> TEAMS.get_by_value('#F00')
<constant 'TEAMS.RED'>

It is allowed for constants to have multiple constants with same values. However, in such case the get_by_value() method will return the first matching constant considering the order constants are defined:

1
2
3
4
class FOO(Values):
  ATTR1     = ValueConstant('one')
  ATTR2     = ValueConstant('two')
  ATTR1_DUB = ValueConstant('one')
5
6
>>> FOO.get_by_value('one')
<constant 'FOO.ATTR1'>

If there is a real need to have multiple constants with same values, it’s possible to get all of them by their value using filter_by_value() method:

7
8
>>> FOO.filter_by_value('one')
[<constant 'FOO.ATTR1'>, <constant 'FOO.ATTR1_DUB'>]

3.3. Verbose constants

Verbose constants are special constants with human-readable names and help messages.

They can be useful when there’s a need to present constants as possible choices to a user.

Usually, this is achieved by defining each constant literal as a separate global variable, followed by construction of a lookup dictionary or tuple:

1
2
3
4
5
6
7
8
9
COUNTRY_AU = 'au'
COUNTRY_UK = 'uk'
COUNTRY_US = 'us'

COUNTRIES_NAMES = (
  (COUNTRY_AU, "Australia"),
  (COUNTRY_UK, "United States"),
  (COUNTRY_US, "United Kingdom"),
)

This is hard to use and to maintain already. And its very common for names to come with descriptions or help texts, which means additional complexity.

In the contrast, it’s possible to use VerboseConstant to keep definitions coupled and concise:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from candv import Constants
from candv import VerboseConstant


class Countries(Constants):
  au = VerboseConstant("Australia")
  uk = VerboseConstant("United Kingdom")
  us = VerboseConstant(
    verbose_name="United States",
    help_text="optional description",
  )

Verbose constants are derived from SimpleConstant in their nature:

12
13
>>> VerboseConstant.mro()
[<class 'candv.ext.VerboseConstant'>, <class 'candv.ext.VerboseMixin'>, <class 'candv.core.SimpleConstant'>, <class 'object'>]

And in addition to the basic attributes of SimpleConstant, instances of VerboseConstant have extra optional attributes:

  • verbose_name
  • help_text
14
15
16
17
18
19
20
21
22
23
24
>>> Countries.au.name
'au'

>>> Countries.au.verbose_name
'Australia'

>>> Countries.au.help_text
None

>>> Countries.us.help_text
'optional description'

Attributes of verbose constants can be lazy translations, for example, provided by verboselib or, say, Django translation strings:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from candv import Constants
from candv import VerboseConstant

from verboselib import Translations


translations = Translations(
  domain="the_app",
  locale_dir_path="locale",
)
_ = translations.gettext_lazy


class UnitType(Constants):
  aircraft = VerboseConstant(_("aircraft"))
  ship     = VerboseConstant(_("ship"))
  train    = VerboseConstant(_("train"))
  vehicle  = VerboseConstant(_("vehicle"))

3.4. Verbose constants with values

Another type of constants supported by candv out of the box are verbose constants with values.

Intuitively, the constant class which allows that is VerboseValueConstant:

Obviously, it needs to be contained by Values or by its derivatives:

1
2
3
4
5
6
7
8
9
from candv import Values
from candv import VerboseValueConstant


class SkillLevel(Values):
  rki = VerboseValueConstant(0, "rookie")
  avg = VerboseValueConstant(1, "average")
  vtn = VerboseValueConstant(2, "veteran")
  ace = VerboseValueConstant(3, "ace")

Here, constants have attributes of both ValueConstant and VerboseConstant:

10
11
12
13
14
15
16
17
18
19
20
>>> VerboseValueConstant.mro()
[<class 'candv.ext.VerboseValueConstant'>, <class 'candv.ext.VerboseMixin'>, <class 'candv.ext.ValueConstant'>, <class 'candv.core.SimpleConstant'>, <class 'object'>]

>>> SkillLevel.avg.name
'avg'

>>> SkillLevel.avg.full_name
'SkillLevel.avg'

>>> SkillLevel.avg.value
1

3.5. Hierarchies

candv library supports an exotic feature of constants hierarchies. This enables creation of subconstants:

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


class TREE(Constants):
  LEFT = SimpleConstant().to_group(Constants,
    LEFT  = SimpleConstant(),
    RIGHT = SimpleConstant(),
  )
  RIGHT = SimpleConstant().to_group(Constants,
    LEFT  = SimpleConstant(),
    RIGHT = SimpleConstant(),
  )

Here, the key point is to_group() method. It turns a constant into a group, which is both a constant and a container.

As for the arguments, the to_group() method accepts a class that will be used to construct new container and instances of constants passed as keywords.

Groups can be created from any constant and any container can be used to store subconstants.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
>>> TREE.LEFT
<constants group 'TREE.LEFT'>

>>> TREE.LEFT.name
'LEFT'

>>> TREE.LEFT.full_name
'TREE.LEFT'

>>> TREE.LEFT.constant_class
<class 'candv.base.Constant'>

>>> TREE.LEFT.names()
['LEFT', 'RIGHT']

>>> TREE.LEFT.LEFT
<constant 'TREE.LEFT.LEFT'>

>>> TREE.LEFT.LEFT.full_name
'TREE.LEFT.LEFT'

>>> TREE.LEFT.LEFT.container
<constants group 'TREE.LEFT'>

3.6. Serialization

There are several ways to serialize candv constants:

  • Using pickle.
  • Converting to a primitive and then to a JSON or similar.

3.6.1. Pickling

Usually, pickling should be avoided. However, there are situations, when it cannot be avoided, e.g., when passing data to and from subprocesses, etc. If pickled objects really can be trusted, they are good to go.

candv constants are pickle-able. For example, there’s a definition of STATUS in a constants.py module:

1
2
3
4
5
6
7
8
# constants.py
from candv import Constants
from candv import SimpleConstant


class STATUS(Constants):
  SUCCESS = SimpleConstant()
  FAILURE = SimpleConstant()

One process can create a variable and pickle it into a file:

1
2
3
4
5
6
7
8
9
import pickle

from constants import STATUS


status = STATUS.SUCCESS

with open('foo.pkl', 'wb') as f:
  pickle.dump(status, f)

And another process can restore the value:

1
2
3
4
import pickle

with open('foo.pkl', 'rb') as f:
  status = pickle.load(f)
5
6
>>> status
<constant 'STATUS.SUCCESS'>

3.6.2. Converting to primitives

New in version 1.3.0.

Constants and containers can be converted into Python primitives for further serialization, for example, into JSONs.

This is done via to_primitive() method.

For example, for simple constants defined previously:

>>> STATUS.to_primitive()
{'name': 'STATUS', 'items': [{'name': 'SUCCESS'}, {'name': 'FAILURE'}]}

>>> STATUS.SUCCESS.to_primitive()
{'name': 'SUCCESS'}

Same for constants with values:

>>> TEAMS.RED.to_primitive()
{'name': 'RED', 'value': '#F00'}

Note

Actual values of constants are out of scope of this library.

Any value can be used as a value of constants, but converting values into primitives is almost up to the user.

If a given value is a callable (e.g., it’s a lazy translation string), candv will call it to get it’s value.

If it has to_primitive(*args, **kwargs) method, again, candv will call it.

If it has isoformat() method (it’s a date, time, etc.), candv will call it either.

Everything else is expected to be a primitive by itself. Otherwise, it’s recommended to implement a custom constant class with custom conversion to primitives.

For verbose constants:

>>> Countries.au.to_primitive()
{'name': 'au', 'verbose_name': 'Australia', 'help_text': None}

For verbose constants with values:

>>> SkillLevel.ace.to_primitive()
{'name': 'ace', 'value': 3, 'verbose_name': 'ace', 'help_text': None}

And for hierarchies:

>>> TREE.to_primitive()
{'name': 'TREE', 'items': [{'name': 'LEFT', 'items': [{'name': 'LEFT'}, {'name': 'RIGHT'}]}, {'name':   'RIGHT', 'items': [{'name': 'LEFT'}, {'name': 'RIGHT'}]}]}

>>> TREE.LEFT.to_primitive()
{'name': 'LEFT', 'items': [{'name': 'LEFT'}, {'name': 'RIGHT'}]}

>>> TREE.LEFT.LEFT.to_primitive()
{'name': 'LEFT'}

3.7. Using with django

It’s possible to use verbose constants and verbose constants with values as choices in djnago models. See django-candv-choices details.

Additionally, see django-rf-candv-choices for using as choices in django-rest-framework.