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 itstype
entry for a type ID and uses it to callnew()
.>>> factory.convert({"type": "sheep", "wool": "lots"}) Sheep(wool='lots')
Notes
convert()
takes aallowed_cls
argument and uses it exactly asnew()
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)}