candv: Constants & Values

Version of PyPI package Supported versions of Python Documentation Status MIT license

Build status of the master branch on Linux Build status of the master branch on Windows Code quality provided by «Codebeat» Code quality provided by «Codacy» Code quality provided by «Scrutinizer CI»

candv allows to create complex enum-like constants.

In contrast to other methods of defining constants, candv helps to organize and to document classes of constants.

This is done by providing iteration and lookup facilities for containers of constants.

Additionally, candv provides an ability to attach human-readable names, descriptions, arbitrary values, and methods to constants.

Inspired by Constants from Twisted and Form Fields from Django.

Contents

Brief Overview

The most basic definition of simple constants:

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


class TEAM(Constants):
  RED  = SimpleConstant()
  BLUE = SimpleConstant()

And usage:

 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
TEAM.RED                 # <constant 'TEAM.RED'>
TEAM['RED']              # <constant 'TEAM.RED'>
TEAM.get('RED')          # <constant 'TEAM.RED'>
TEAM.get('GREEN')        # None
TEAM.RED.name            # 'RED'
TEAM.RED.full_name       # 'TEAM.RED'
TEAM.RED.to_primitive()  # {'name': 'RED'}
TEAM.RED.container       # <constants container 'TEAM'>

TEAM                     # <constants container 'TEAM'>
TEAM.name                # 'TEAM'
TEAM.full_name           # 'TEAM'
len(TEAM)                # 2
TEAM.has_name('RED')     # True

TEAM.names()             # ['RED', 'BLUE']
TEAM.iternames()         # <odict_iterator object at 0x7f451013e0e0>

TEAM.constants()         # [<constant 'TEAM.RED'>, <constant 'TEAM.BLUE'>]
TEAM.iterconstants()     # <odict_iterator object at 0x7f45100f3450>

TEAM.items()             # [('RED', <constant 'TEAM.RED'>), ('BLUE', <constant 'TEAM.BLUE'>)]
TEAM.iteritems()         # <odict_iterator object at 0x7f451013bdb0>

TEAM.to_primitive()      # {'name': 'TEAM', 'items': [{'name': 'RED'}, {'name': 'BLUE'}]}

Using with values:

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


class TEAM(Values):
  RED  = ValueConstant(1)
  BLUE = ValueConstant(2)


TEAM.values()            # [1, 2]
TEAM.itervalues()        # <map object at 0x7f450ffdb1c0>

TEAM.get_by_value(1)     # <constant 'TEAM.RED'>
TEAM.filter_by_value(1)  # [<constant 'TEAM.RED'>]

TEAM.RED.value           # 1
TEAM.RED.to_primitive()  # {'name': 'RED', 'value': 1}

Using with human-readable names:

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


class Countries(Constants):
  au = VerboseConstant("Australia")
  uk = VerboseConstant("United Kingdom")
  us = VerboseConstant("United States")


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

With values and names:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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")


SkillLevel.avg.value           #  1
SkillLevel.avg.name            # 'avg'
SkillLevel.avg.full_name       # 'SkillLevel.avg'
SkillLevel.avg.verbose_name    # 'average'
SkillLevel.avg.help_text       # None
SkillLevel.avg.to_primitive()  # {'name': 'avg', 'value': 1, 'verbose_name': 'average', 'help_text': None}

Plays well with 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"))

Supports custom methods:

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


class SupportedLanguages(Constants):
  en = SimpleConstant()
  ru = SimpleConstant()

  @classmethod
  def get_default(cls):
    return cls.en


SupportedLanguages.get_default()  # <constant 'SupportedLanguages.en'>

And custom types of constants:

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


class MissionStatus(SimpleConstant):
  ...


class MissionStatuses(with_constant_class(MissionStatus), Constants):
  not_loaded = MissionStatus()
  loaded     = MissionStatus()
  playing    = MissionStatus()

It’s also possible to define hierarchies:

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


class STATUS(Constants):
  SUCCESS = SimpleConstant()
  ERROR   = SimpleConstant().to_group(Constants,

    INVALID   = SimpleConstant(),
    NOT_FOUND = SimpleConstant(),
    INTERNAL  = SimpleConstant(),
  )


STATUS.names()                   # ['SUCCESS', 'ERROR']
STATUS.ERROR                     # <constants group 'STATUS.ERROR'>
STATUS.ERROR.full_name           # 'STATUS.ERROR'
STATUS.ERROR.INTERNAL            # <constant 'STATUS.ERROR.INTERNAL'>
STATUS.ERROR.INTERNAL.full_name  # 'STATUS.ERROR.INTERNAL'
STATUS.ERROR.names()             # ['INVALID', 'NOT_FOUND', 'INTERNAL']

Installation

Available as a PyPI package:

pip install candv

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.

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'>

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'>]

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"))

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

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'>

Serialization

There are several ways to serialize candv constants:

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

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'>

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'}

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.

Customization

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

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.

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.

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.

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.

Changelog

  • 1.5.0 (Nov 18, 2020)

    API changes:

    • to_primitive() methods use *args and **kwargs instead of the context param.
  • 1.4.0 (Oct 30, 2020)

    API changes: public API is not changed, however, the following internal changes are introduced:

    1. candv.base is moved to candv.core.
    2. Package-level definitions in candv are moved to candv.ext.
    3. candv.version module is added.
    4. Package-specific exceptions are defined in candv.exceptions. Work as before, but now exceptions can be caught more precisely if needed.
    5. candv.SimpleConstant is a direct reference to candv.core.SimpleConstant now. Previously it was an alias to candv.base.Constant.
    6. candv.Constants is a direct reference to candv.core.Constants now. Previously it was an alias to candv.base.ConstantsContainer.

    Python support:

    • Support of all Python versions below 3.7 is dropped.

    Other:

    1. All external dependencies are removed.
    2. The license is switched from LGPLv3 to MIT.
    3. The documentation is reworked to be more explanatory and concise.
  • 1.3.1 (Aug 1, 2015)

    • Comparison of constants is fixed: now it is based on constant’s full_name attribute (issue #11).
  • 1.3.0 (Dec 31, 2014)

    • to_primitive() method is implemented. This can be used for serialization, for example, into JSON (issue #1). See usage and customization for more info.
  • 1.2.0 (Oct 11, 2014)

    1. Core classes are significantly refactored.
    2. constant_class now uses candv.SimpleConstant as the default value (instead of None, see Customization for more info).
    3. Support of groups is reimplemented: now they are classes just as other constants containers (previously groups were instances of patched containers). So, groups automatically gain all of those attributes and methods which usual containers have.
    4. Constant’s container attribute is public now. Groups of constants have it too (see Hierarchies).
    5. API of containers is made really close to API of Python’s dict (see usage for more info):
      • __getitem__, __contains__, __len__ and __iter__ magic methods are implemented.
      • contains method is renamed to has_name.
      • get_by_name method is removed in favor of __getitem__ method.
      • get method with support of default value is introduced.
    6. All objects (containers, groups and constants) have name and full_name attributes now. This may be useful if names of constants are used as key values (e.g. for Redis).
    7. Also, all objects have good repr now.
    8. Mixin factory candv.with_constant_class() is introduced. It may help to define containers in a more compact way.
    9. A potential bug of uninitialized unbounded constants is fixed. Unbounded constant is an instance of a class which differs from container’s constant_class or its subclasses. This is unnatural case, but if this is really needed, it will not break now.
    10. Exception messages are more informative now.
    11. Tests are moved out the package.
    12. Introductory documentation is improved. Other docs are updated too.
  • 1.1.2 (Jul 6, 2014)

    • values and itervalues attributes are added to Constants.
  • 1.1.1 (Jun 21, 2014)

    • switch license from GPLv2 to LGPLv3.
  • 1.1.0 (Jun 21, 2014)

    1. Choices container is moved to django-candv-choices library.
    2. Docs are updated and typos are fixed.
    3. Utils are stripped from requirements.
  • 1.0.0 (Apr 15, 2014)

    • Initial version.

Sources

Feel free to explore, fork or contribute: https://github.com/oblalex/candv

API

candv package

Submodules

candv.core module

Defines base constant and base container for constants.

class candv.core.SimpleConstant[source]

Bases: object

Base class for all constants.

Variables:name (str) – constant’s name: set up automatically and is equal to the name of the container’s attribute
full_name
merge_into_group(group)[source]

Called automatically by the container after group construction.

Note

Redefine this method in all derived classes. Attach all custom attributes and methods to the group here.

Parameters:group – an instance of Constants or of its subclass into which this constant will be merged
Returns:None
to_group(group_class, **group_members)[source]

Convert a constant into a constants group.

Parameters:
  • group_class (class) – a class of group container which will be created
  • group_members – unpacked dict which defines group members
Returns:

a lazy constants group which will be evaluated by the container. Method merge_into_group() will be called during evaluation of the group

Example:

from candv import Constants
from candv import SimpleConstant


class FOO(Constants):
  A = SimpleConstant()
  B = SimpleConstant().to_group(Constants,

    B2 = SimpleConstant(),
    B0 = SimpleConstant(),
    B1 = SimpleConstant(),
  )
to_primitive(*args, **kwargs)[source]

Represent the constant via Python’s primitive data structures.

Changed in version 1.5.0: The context param is replaced by *args and **kwargs.

New in version 1.3.0.

class candv.core.Constants[source]

Bases: object

Base class for creating constants containers.

Each constant defined within the container will remember its creation order.

See an example in constants().

Variables:constant_class – defines a class of constants which a container will store. This attribute MUST be set up manually when you define a new container type. Otherwise container will not be initialized. Default: None
Raises:CandvContainerMisusedError – if you try to create an instance of container. Containers are singletons and they cannot be instantiated. Their attributes must be used directly.
constant_class

Defines a top-level class of constants which can be stored by container

alias of SimpleConstant

full_name = 'Constants'
name = 'Constants'
candv.core.with_constant_class(the_class)[source]

Create a mixin class with constant_class attribute.

Allows to set a constant class for constants container outside container itself. This may help to create more readable container definition, e.g.:

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


class CustomConstant(SimpleConstant):
  ...

class FOO(with_constant_class(CustomConstant), Constants):
  A = CustomConstant()
  B = CustomConstant()
>>> FOO.constant_class
<class '__main__.CustomConstant'>

candv.exceptions module

exception candv.exceptions.CandvException[source]

Bases: Exception

Base exception

New in version 1.4.0.

exception candv.exceptions.CandvTypeError[source]

Bases: TypeError, candv.exceptions.CandvException

New in version 1.4.0.

exception candv.exceptions.CandvInvalidGroupMemberError[source]

Bases: candv.exceptions.CandvTypeError

New in version 1.4.0.

exception candv.exceptions.CandvInvalidConstantClass[source]

Bases: candv.exceptions.CandvTypeError

New in version 1.4.0.

exception candv.exceptions.CandvContainerMisusedError[source]

Bases: candv.exceptions.CandvTypeError

New in version 1.4.0.

exception candv.exceptions.CandvConstantAlreadyBoundError[source]

Bases: ValueError, candv.exceptions.CandvException

New in version 1.4.0.

exception candv.exceptions.CandvMissingConstantError[source]

Bases: KeyError, candv.exceptions.CandvException

New in version 1.4.0.

exception candv.exceptions.CandvValueNotFoundError[source]

Bases: ValueError, candv.exceptions.CandvException

New in version 1.4.0.

candv.ext module

Provides extra ready-to-use classes for constructing custom constants.

class candv.ext.VerboseMixin(*args, **kwargs)[source]

Bases: object

Adds verbose_name and help_text attributes to constants.

Arguments must be passed as kwargs.

Parameters:
  • verbose_name (str) – optional verbose name
  • help_text (str) – optional description

Example:

class CustomConstant(object):

  def __init__(self, arg1, arg2, kwarg1=None):
    pass


class VerboseCustomConstant(VerboseMixin, CustomConstant):

  def __init__(self, arg1, arg2, kwarg1=None, verbose_name=None, help_text=None):
    super().__init__(
      arg1,
      arg2,
      kwarg1=kwarg1,
      verbose_name=verbose_name,
      help_text=help_text,
    )
merge_into_group(group)[source]

Overrides merge_into_group() to add verbose_name with help_text attributes to the target group.

to_primitive(*args, **kwargs)[source]

Changed in version 1.5.0: The context param is replaced by *args and **kwargs.

New in version 1.3.0.

class candv.ext.VerboseConstant(verbose_name=None, help_text=None)[source]

Bases: candv.ext.VerboseMixin, candv.core.SimpleConstant

A constant with optional verbose name and optional help text.

Parameters:
  • verbose_name (str) – optional verbose name of the constant
  • help_text (str) – optional description of the constant
Variables:
  • verbose_name (str) – verbose name of the constant. Default: None
  • help_text (str) – verbose description of the constant. Default: None
class candv.ext.ValueConstant(value)[source]

Bases: candv.core.SimpleConstant

A constant with ability to hold arbitrary values.

Parameters:value – a value to attach to constant
Variables:value – constant’s value
merge_into_group(group)[source]

Redefines merge_into_group() and adds value attribute to the target group.

to_primitive(*args, **kwargs)[source]

Changed in version 1.5.0: The context param is replaced by *args and **kwargs.

New in version 1.3.0.

class candv.ext.VerboseValueConstant(value, verbose_name=None, help_text=None)[source]

Bases: candv.ext.VerboseMixin, candv.ext.ValueConstant

A constant which can have both verbose name, help text, and a value.

Parameters:
  • value – a value to attach to the constant
  • verbose_name (str) – an optional verbose name of the constant
  • help_text (str) – an optional description of the constant
Variables:
  • value – constant’s value
  • verbose_name (str) – verbose name of the constant. Default: None
  • help_text (str) – verbose description of the constant. Default: None
class candv.ext.Values[source]

Bases: candv.core.Constants

A container for ValueConstant and its derivatives.

Supports getting and filtering constants by their values plus listing values of all constants in container.

constant_class

alias of ValueConstant

classmethod filter_by_value(value)[source]

Get all constants which have given value.

Parameters:value – value of the constants to look for
Returns:list of all found constants with given value
full_name = 'Values'
classmethod get_by_value(value)[source]

Get a constant by its value.

Parameters:value – value of the constant to look for
Returns:first found constant with given value
Raises:CandvValueNotFoundError – if no constant in container has given value
classmethod itervalues()[source]

Get an iterator over values of all constants in the order they were defined.

Same as values() but returns an interator.

Note

Overrides itervalues() since 1.1.2.

name = 'Values'
classmethod values()[source]

List values of all constants in the order they were defined.

Returns:list of values

Example:

from candv import Values
from candv import ValueConstant


class FOO(Values):
  TWO  = ValueConstant(2)
  ONE  = ValueConstant(1)
  SOME = ValueConstant("some string")
>>> FOO.values()
[2, 1, 'some string']

Note

Overrides values() since 1.1.2.

candv.version module

Module contents

Indices and tables