Skip to content

Commit

Permalink
Factory: New aliasing system
Browse files Browse the repository at this point in the history
  • Loading branch information
leroyvn committed Jul 29, 2022
1 parent 87949f5 commit 1b9d6fb
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 44 deletions.
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

[![CalVer](https://img.shields.io/badge/calver-YY.MINOR.MICRO-blue?style=flat-square)](https://calver.org/)

## Dessine-moi 22.1.1 (unreleased)
## Dessine-moi 22.2.0 (unreleased)

## Fixes and improvements

- `LazyType.convert()`: Resolve lazy types ({ghpr}`5`).
- `Factory`: New alias system ({ghpr}`6`).
- `Factory.register()`: Rename `allow_id_overwrite` to `overwrite_id` ({ghpr}``6).

## Dessine-moi 22.1.1 (2022-07-28)

### Fixes and improvements

- `LazyType.load()`: Replace `__import__()` with `importlib.import_module()`
({ghpr}`4`).

Expand Down Expand Up @@ -37,7 +43,6 @@

- `Factory.create()`: Add `construct` keyword argument ({ghcommit}`a2e5874`).


## Dessine-moi 21.1.0 (2021-06-02)

Initial release.
40 changes: 26 additions & 14 deletions docs/rst/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,37 @@ class attribute to set the class's ID in the registry:
.. note:: When used as a decorator, :meth:`~.Factory.register` is best used
last (*i.e.* at the top of the sequence).

The :meth:`~.Factory.register` method implements safeguards which can be
bypassed with dedicated keyword arguments:
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.

* if ``allow_aliases`` is ``True``, a type can be registered multiple times with
different IDs (the default value is ``False``):
The :meth:`~.Factory.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.

.. doctest::
Alias registered types
^^^^^^^^^^^^^^^^^^^^^^

>>> factory.register(Sheep, type_id="mouton", allow_aliases=True)
<class '__main__.Sheep'>
>>> 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)})
Having multiple IDs pointing to the same registered type may be useful as well.
Types can be aliased after registration using the :meth:`~.Factory.alias`
method:

* if ``allow_id_overwrite`` is ``True``, registering a type with an existing ID
will succeed and overwrite the existing entry (the default value is ``False``).
.. doctest::

Finally, :meth:`~.Factory.register` 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.
>>> 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 :meth:`~.Factory.register`'s ``aliases``
keyword argument.

.. doctest::

>>> 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
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
63 changes: 51 additions & 12 deletions src/dessinemoi/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ def _register_impl(
cls: Union[Type, LazyType, str],
type_id: Optional[str] = None,
dict_constructor: Optional[str] = None,
allow_aliases: bool = False,
allow_id_overwrite: bool = False,
aliases: Optional[List[str]] = None,
overwrite_id: bool = False,
allow_lazy: bool = True,
) -> Any:
if isinstance(cls, str):
Expand All @@ -176,11 +176,11 @@ def _register_impl(

# Check if type is already registered
cls_fullname = _fullname(cls)
if not allow_aliases and cls_fullname in self.registered_types:
if not aliases and cls_fullname in self.registered_types:
raise ValueError(f"'{cls_fullname}' is already registered")

# Check if ID is already used
if not allow_id_overwrite and type_id in self.registry.keys():
if not overwrite_id and type_id in self.registry.keys():
raise ValueError(
f"'{type_id}' is already used to reference "
f"'{_fullname(self.registry[type_id].cls)}'"
Expand All @@ -201,6 +201,13 @@ def _register_impl(
dict_constructor=dict_constructor,
)

# Add aliases
if aliases is None:
aliases = []

for alias_id in aliases:
self.alias(type_id, alias_id)

return cls

def register(
Expand All @@ -209,8 +216,8 @@ def register(
*,
type_id: Optional[str] = None,
dict_constructor: Optional[str] = None,
allow_aliases: bool = False,
allow_id_overwrite: bool = False,
aliases: Optional[List[str]] = None,
overwrite_id: bool = False,
allow_lazy: bool = True,
) -> Any:
"""
Expand All @@ -235,11 +242,11 @@ class decorator. A :class:`LazyType` instance or a string
Class method to be used for dictionary-based construction. If
``None``, the default constructor is used.
:param allow_aliases:
:param aliases:
If ``True``, a given type can be registered multiple times under
different IDs.
:param allow_id_overwrite:
:param overwrite_id:
If ``True``, existing IDs can be overwritten.
:param allow_lazy:
Expand All @@ -261,6 +268,10 @@ class decorator. A :class:`LazyType` instance or a string
.. versionchanged:: 22.1.0
Added ``allow_lazy`` argument. Accept :class:`LazyType` and strings
for ``cls``.
.. versionchanged:: 22.2.0
Renamed ``allow_id_overwrite`` to ``overwrite_id``.
Removed ``allow_aliases``, replaced by ``aliases``.
"""

if cls is not _MISSING:
Expand All @@ -269,8 +280,8 @@ class decorator. A :class:`LazyType` instance or a string
cls,
type_id=type_id,
dict_constructor=dict_constructor,
allow_aliases=allow_aliases,
allow_id_overwrite=allow_id_overwrite,
aliases=aliases,
overwrite_id=overwrite_id,
allow_lazy=allow_lazy,
)
except ValueError:
Expand All @@ -283,12 +294,40 @@ def inner_wrapper(wrapped_cls):
wrapped_cls,
type_id=type_id,
dict_constructor=dict_constructor,
allow_aliases=allow_aliases,
allow_id_overwrite=allow_id_overwrite,
aliases=aliases,
overwrite_id=overwrite_id,
)

return inner_wrapper

def alias(self, type_id: str, alias_id: str, overwrite_id: bool = False) -> None:
"""
Register a new alias to a registered type.
:param type_id:
ID of the aliased type.
:param alias_id:
Created alias ID.
:raises ValueError:
.. versionadded:: 22.2.0
"""
if type_id in self.registry:
if not overwrite_id and alias_id in self.registry.keys():
raise ValueError(
f"'{type_id}' is already used to reference "
f"'{_fullname(self.registry[type_id].cls)}'"
)

else:
self.registry[alias_id] = self.registry[type_id]

else:
raise ValueError(f"cannot alias unregistered type '{type_id}'")

def get_type(self, type_id: str) -> Type:
"""
Return the type corresponding to the requested type ID. Lazy types will
Expand Down
56 changes: 40 additions & 16 deletions tests/test_dessinemoi.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,28 +32,52 @@ class Sheep:
cls=Sheep, dict_constructor=None
)

# Registering class again fails if aliases are not allowed
# Registering class again fails
with pytest.raises(ValueError):
factory.register(Sheep, type_id="mouton")
factory.register(Sheep, type_id="mouton", allow_aliases=True)
assert factory.registry["mouton"] == dessinemoi.FactoryRegistryEntry(
cls=Sheep, dict_constructor=None
)

# Overwriting existing ID fails if not explicitly allowed
with pytest.raises(ValueError):
factory.register(int, type_id="sheep")
factory.register(int, type_id="sheep", allow_id_overwrite=True)
factory.register(int, type_id="sheep", overwrite_id=True)

# A new class can also be registered with a decorator
# Decorator uses can also be chained
@factory.register(type_id="agneau", allow_aliases=True) # Full function call form
@factory.register # Optionless form
class Lamb(Sheep):
_TYPE_ID = "lamb"

assert "lamb" in factory.registry
assert factory.registry["lamb"].cls is Lamb

@factory.register(type_id="agneau") # Full form
class Agneau(Sheep):
pass

assert "agneau" in factory.registry
assert factory.registry["agneau"].cls is Agneau


def test_factory_alias(factory):
# Registering an alias to a nonexisting type fails
with pytest.raises(ValueError):
factory.alias("lamb", "agneau")

# Aliasing an existing type works as expected
@factory.register(type_id="lamb")
class Lamb:
pass

factory.alias("lamb", "agneau")
assert "agneau" in factory.registry
assert factory.registry["agneau"].cls is Lamb

# Aliasing a type with an existing type ID fails
with pytest.raises(ValueError):
factory.alias("lamb", "agneau")

# Aliases can be defined upon registration
factory.registry.clear()
factory.register(Lamb, type_id="lamb", aliases=["agneau"])
assert factory.registry["lamb"].cls is Lamb
assert factory.registry["agneau"].cls is Lamb

Expand All @@ -72,39 +96,39 @@ def test_factory_lazy(factory):
assert factory.registry["lazy"].cls is LazyTypeTest

# A LazyType instance can be registered and is resolved upon call to create()
del factory.registry["lazy"]
factory.register(
LazyType(__name__, "LazyTypeTest"),
type_id="lazy",
allow_id_overwrite=True,
allow_aliases=True,
)
assert isinstance(factory.registry["lazy"].cls, dessinemoi.LazyType)
assert isinstance(factory.create("lazy"), LazyTypeTest)
# After dereferencing, the lazy type is replaced by the actual type
assert factory.registry["lazy"].cls is LazyTypeTest

# Strings are interpreted as lazy types
del factory.registry["lazy"]
factory.register(
f"{__name__}.LazyTypeTest",
type_id="lazy",
allow_id_overwrite=True,
allow_aliases=True,
overwrite_id=True,
)
assert isinstance(factory.create("lazy"), LazyTypeTest)

del factory.registry["lazy"]
factory.register(
f"{__name__}.LazyTypeTest",
type_id="lazy",
allow_id_overwrite=True,
allow_aliases=True,
overwrite_id=True,
)
assert isinstance(factory.create("lazy"), LazyTypeTest)

# Lazy types require an ID
aliases = (True,)
with pytest.raises(ValueError):
factory.register(
f"{__name__}.LazyTypeTest",
allow_id_overwrite=True,
allow_aliases=True,
overwrite_id=True,
)


Expand Down

0 comments on commit 1b9d6fb

Please sign in to comment.