Skip to content

Commit

Permalink
refactor(namespace): Remove the intermediary _Namespace model
Browse files Browse the repository at this point in the history
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)`.
  • Loading branch information
twidi committed Sep 28, 2020
1 parent f5cfe92 commit 27e4ea0
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 38 deletions.
Expand Up @@ -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
Expand All @@ -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
Expand Down
53 changes: 44 additions & 9 deletions isshub/domain/utils/entity.py
Expand Up @@ -9,13 +9,38 @@
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
-------
Expand All @@ -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)
),
)


Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
-------
Expand All @@ -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'
Expand All @@ -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):
Expand Down

0 comments on commit 27e4ea0

Please sign in to comment.