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