Usage#

Create a factory object#

To start using Dessine-moi’s factories, the first thing to do is to instantiate the Factory class:

>>> import dessinemoi
>>> factory = dessinemoi.Factory()

Our factory holds a registry, which maps identifiers (IDs), consisting of strings, to Python types. Our factory has currently an empty registry:

>>> factory
Factory(registry={})

Register types to the factory#

Let’s define a new Python type. For convenience, we will work with the attrs library, but Dessine-moi does not require you to use it. Let’s define a simple class:

>>> import attrs
>>> @attrs.define
... class Sheep:
...     wool = attrs.field(default="some")

We can now register this class to the factory using the register() method:

>>> factory.register(Sheep, type_id="sheep")
<class '__main__.Sheep'>
>>> factory
Factory(registry={'sheep': FactoryRegistryEntry(cls=<class '__main__.Sheep'>, dict_constructor=None)})

Classes can also be registered upon declaration using register() as a decorator. In that case, the cls positional argument is omitted. If the type_id keyword argument is omitted, Dessine-moi looks for a _TYPE_ID class attribute to set the class’s ID in the registry:

>>> @factory.register
... @attrs.define
... class Lamb(Sheep):
...     _TYPE_ID = "lamb"
>>> factory
Factory(registry={'sheep': FactoryRegistryEntry(cls=<class '__main__.Sheep'>, dict_constructor=None), 'lamb': FactoryRegistryEntry(cls=<class '__main__.Lamb'>, dict_constructor=None)})

Note

As can be seen from the previous code snippet, the call operator () can be omitted if all arguments are omitted.

Note

When used as a decorator, register() is best used last (i.e. at the top of the sequence).

By default, ID overwrite is not allowed. The overwrite_id parameter can be set to True to force the registration of a type with an existing ID.

The register() method features an optional dict_constructor argument which, when set, associates a class method constructor to be called upon attempting dictionary conversion. See Convert objects for more detail.

Alias registered types#

Having multiple IDs pointing to the same registered type may be useful as well. Types can be aliased after registration using the alias() method:

>>> factory.alias("sheep", "mouton")
>>> factory
Factory(registry={'sheep': FactoryRegistryEntry(cls=<class '__main__.Sheep'>, dict_constructor=None), 'lamb': FactoryRegistryEntry(cls=<class '__main__.Lamb'>, dict_constructor=None), 'mouton': FactoryRegistryEntry(cls=<class '__main__.Sheep'>, dict_constructor=None)})

Aliases may also be created using register()’s aliases keyword argument.

>>> del factory.registry["sheep"]
>>> del factory.registry["mouton"]
>>> factory.register(Sheep, type_id="sheep", aliases=["mouton"])
<class '__main__.Sheep'>
>>> factory
Factory(registry={'lamb': FactoryRegistryEntry(cls=<class '__main__.Lamb'>, dict_constructor=None), 'sheep': FactoryRegistryEntry(cls=<class '__main__.Sheep'>, dict_constructor=None), 'mouton': FactoryRegistryEntry(cls=<class '__main__.Sheep'>, dict_constructor=None)})

Instantiate registered types#

Once a type is registered, it can be instantiated using the new() method. If constructed class’s constructor expects arguments, the args and kwargs arguments will forward them appropriately:

>>> merino = factory.create("sheep", kwargs={"wool": "lots"})
>>> merino
Sheep(wool='lots')

If you want to restrict the set of allowed types, the allowed_cls argument is here:

>>> factory.create("sheep", allowed_cls=Lamb)
Traceback (most recent call last):
...
TypeError: 'sheep' does not reference allowed type <class '__main__.Lamb'> or any of its subtypes

Note

Under the hood, allowed_cls is passed to a call to isinstance(): it can therefore be a single type or a tuple of allowed types.

Any subtype of an allowed type is allowed:

>>> factory.create("lamb", allowed_cls=Sheep)
Lamb(wool='some')

Note

A very common and Pythonic design pattern consists in defining special constructors using class methods. If you use this approach, Dessine-moi lets you select a constructor using the construct argument. For demonstrative purposes, let us attach a class method constructor to our Sheep class:

>>> @classmethod
... def unsheavable(cls):
...     return cls(wool="none")
>>> Sheep.unsheavable = unsheavable

We can now route object creation to this function using the construct keyword argument. Since the unsheavable() class method takes no argument, we do not pass the args and kwargs arguments:

>>> factory.create("sheep", construct="unsheavable")
Sheep(wool='none')

Convert objects#

Dessine-moi’s factories implement converters which can be used as part of the attrs conversion step. In its most straightforward form, the convert() method operates on a value argument.

  • If value is not a dictionary, convert() returns it unchanged.

  • If value is a dictionary, convert() queries its type entry for a type ID and uses it to call new().

    >>> factory.convert({"type": "sheep", "wool": "lots"})
    Sheep(wool='lots')
    

Notes

  • convert() takes a allowed_cls argument and uses it exactly as new() does.

  • Dictionary conversion won’t work with classes expected non kw-only fields.

  • If a dict_constructor is associated to the registered type, it will be used to create the object instead of the default constructor.

    >>> factory.registry.clear()
    >>> factory.register(Sheep, type_id="sheep", dict_constructor="unsheavable")
    <class '__main__.Sheep'>
    >>> factory.convert({"type": "sheep"})
    Sheep(wool='none')
    

Extend factories#

Arguably, convert() is rather limited. For instance, it works only for classes whose constructors only take keyword arguments and reserves the type entry for factory ID specification. One could wish to change some of that.

Fortunately, implementing custom conversion methods is simple: subclass Factory and reimplement its convert() method!

Lazy registration#

Sometimes, registering a type to a factory without importing it may be desirable. This is useful, for instance, when it is not sure that the registered type will be used and therefore the import overhead may simply be unnecessary.

Dessine-moi supports lazy registration, which defers type import to instantiation by the factory. This comes at the cost of some of the safety checks, because no detailed information about the registered type will be available.

Lazy registration can be performed by passing the fully qualified name of the target type as a string:

>>> factory.registry.clear()
>>> factory.register("datetime.datetime", type_id="datetime")
LazyType(mod='datetime', attr='datetime')

At this stage, the datetime.datetime class is not imported, it is simply referenced by a LazyType instance.

>>> factory.registry
{'datetime': FactoryRegistryEntry(cls=LazyType(mod='datetime', attr='datetime'), dict_constructor=None)}

If we call Factory.create(), the target type is imported and returned:

>>> factory.create("datetime", args=(2222, 2, 22))
datetime.datetime(2222, 2, 22, 0, 0)

Since the type is imported, its registry entry is also updated:

>>> factory.registry
{'datetime': FactoryRegistryEntry(cls=<class 'datetime.datetime'>, dict_constructor=None)}