refactor(namespace): Remove the intermediary _Namespace model¶
Description¶
Abstract¶
Remove the _Namespace model, and use self as field type when calling optional_field for the Namespace.namespace field.
Motivation¶
Using the intermediary model _Namespace was a hack to be able to pass a defined model as field type to optional_field for the Namespace.namespace field.
Rationale¶
The instance_of validator of the attrs package takes a type. But by subclassing it and using the class from the field instance at validation time, it was possible to create our own validator (via the instance_of_self function, or passing self to optional_field and required_field).
Info¶
- Hash
27e4ea0aef91c7e16f966afdb6973f1b29ef6e9f
- Date
2020-09-28 18:05:22 +0200
- Parents
docs(bdd): Add BDD scenarios to documentation [f5cfe92b] — 2020-09-27 22:43:49 +0200
- Children
docs(domain): Add diagram of entities for each domain context [bb5e73eb] — 2020-10-04 11:50:37 +0200
- Branches
- Tags
(No tags)
Changes¶
isshub/domain/contexts/code_repository/entities/namespace/__init__.py¶
- Type
Modified
- Stats
+2 -29
@@ -21,14 +21,9 @@ class NamespaceKind(enum.Enum):
@validated() # type: ignore
-class _Namespace(BaseModelWithId):
+class Namespace(BaseModelWithId):
"""A namespace can contain namespaces and repositories.
- Notes
- -----
- This is a base class, used by `Namespace` to be able to have a self-reference for the type
- of the `namespace` field.
-
Attributes
----------
id : int
@@ -45,32 +40,10 @@ class _Namespace(BaseModelWithId):
"""
name: str = required_field(str) # type: ignore
- namespace = None
+ namespace: Optional["Namespace"] = optional_field("self") # type: ignore
kind: NamespaceKind = required_field(NamespaceKind) # type: ignore
description: str = optional_field(str) # type: ignore
-
-@validated() # type: ignore
-class Namespace(_Namespace):
- """A namespace can contain namespaces and repositories.
-
- Attributes
- ----------
- id : int
- The unique identifier of the namespace
- name : str
- The name of the namespace. Unique in its parent namespace.
- namespace : Optional[Namespace]
- Where the namespace can be found.
- kind : NamespaceKind
- The kind of namespace.
- description : Optional[str]
- The description of the namespace.
-
- """
-
- namespace: Optional[_Namespace] = optional_field(_Namespace) # type: ignore
-
@field_validator(namespace) # type: ignore
def validate_namespace_is_not_in_a_loop( # noqa # pylint: disable=unused-argument
self, field: Any, value: Any
isshub/domain/utils/entity.py¶
- Type
Modified
- Stats
+44 -9
@@ -9,13 +9,38 @@ It is an adapter over the ``attrs`` external dependency.
import attr
+class _InstanceOfSelfValidator(
+ attr.validators._InstanceOfValidator # pylint: disable=protected-access
+):
+ """Validator checking that the field holds an instance of its own model."""
+
+ def __call__(self, inst, attr, value): # pylint: disable=redefined-outer-name
+ """Validate that the `value` is an instance of the class of `inst`.
+
+ For the parameters, see ``attr.validators._InstanceOfValidator``
+ """
+ self.type = inst.__class__
+ super().__call__(inst, attr, value)
+
+
+def instance_of_self() -> _InstanceOfSelfValidator:
+ """Return a validator checking that the field holds an instance of its own model.
+
+ Returns
+ -------
+ _InstanceOfSelfValidator
+ The instantiated validator
+ """
+ return _InstanceOfSelfValidator(type=None)
+
+
def optional_field(field_type):
"""Define an optional field of the specified `field_type`.
Parameters
----------
- field_type : type
- The expected type of the field when not ``None``.
+ field_type : Union[type, str]
+ The expected type of the field. Use the string "self" to reference the current field's model
Returns
-------
@@ -37,7 +62,11 @@ def optional_field(field_type):
"""
return attr.ib(
default=None,
- validator=attr.validators.optional(attr.validators.instance_of(field_type)),
+ validator=attr.validators.optional(
+ instance_of_self()
+ if field_type == "self"
+ else attr.validators.instance_of(field_type)
+ ),
)
@@ -46,8 +75,8 @@ def required_field(field_type, frozen=False):
Parameters
----------
- field_type : type
- The expected type of the field.
+ field_type : Union[type, str]
+ The expected type of the field. Use the string "self" to reference the current field's model
frozen : bool
If set to ``False`` (the default), the field can be updated after being set at init time.
If set to ``True``, the field can be set at init time but cannot be changed later, else a
@@ -70,7 +99,11 @@ def required_field(field_type, frozen=False):
>>> check_field_not_nullable(MyModel, 'my_field', my_field='foo')
"""
- kwargs = {"validator": attr.validators.instance_of(field_type)}
+ kwargs = {
+ "validator": instance_of_self()
+ if field_type == "self"
+ else attr.validators.instance_of(field_type)
+ }
if frozen:
kwargs["on_setattr"] = attr.setters.frozen
@@ -80,7 +113,9 @@ def required_field(field_type, frozen=False):
def validated():
"""Decorate an entity to handle validation.
- This will let ``attrs`` manage the class, using slots for fields.
+ This will let ``attrs`` manage the class, using slots for fields, and forcing attributes to
+ be passed as named arguments (this allows to not have to defined all required fields first, then
+ optional ones, and resolves problems with inheritance where we can't handle the order)
Returns
-------
@@ -101,7 +136,7 @@ def validated():
>>> instance = MyModel()
Traceback (most recent call last):
...
- TypeError: __init__() missing 1 required positional argument: 'my_field'
+ TypeError: __init__() missing 1 required keyword-only argument: 'my_field'
>>> instance = MyModel(my_field='foo')
>>> instance.my_field
'foo'
@@ -113,7 +148,7 @@ def validated():
TypeError: ("'my_field' must be <class 'str'> (got None that is a <class 'NoneType'>)...
"""
- return attr.s(slots=True)
+ return attr.s(slots=True, kw_only=True)
def field_validator(field):