Skip to content

Commit 70534c9

Browse files
committed
feature: Nesting ErrorCatalogs
1 parent 19c2f59 commit 70534c9

5 files changed

Lines changed: 97 additions & 18 deletions

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
### 0.2.0
88

9-
- [ ] Introducing nested catalogs
9+
- [x] Introducing nested catalogs
1010
- [ ] Beta release
1111

1212
### 1.0.0

pca/packages/errors/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
from .types import * # noqa: F401, F403
55

66

7-
VERSION = (0, 1, 0)
7+
VERSION = (0, 1, 0, "alpha", 0)

pca/packages/errors/builder.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,9 @@ class ErrorMeta(type):
7070
* human-readable description of the error should be computed as late as possible (not earlier
7171
than on the presentation layer, where l10n & i18n process is made)
7272
* error instance should have a unique code
73-
* error instance can have an area which describes its general topic
7473
* error instances can be gathered into catalogs which describe their common reason or a place
7574
to be raised
76-
* an error instance is a value object, defined by their code and area
75+
* an error instance is a value object, defined by their code
7776
* an error can have params, which can be used to pass some data specific for the place
7877
the instance is raised, but isn't considered a part of the value for checking instance
7978
equality

pca/packages/errors/catalog.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@
1010

1111
class ErrorCatalogMeta(type):
1212

13-
_registry: t.Dict[str, ExceptionWithCodeType]
13+
__error_registry: t.Dict[str, ExceptionWithCodeType]
14+
__subcatalog_registry: t.Dict[str, "ErrorCatalogMeta"]
1415

1516
def __init__(self, *args, **kwargs):
1617
super().__init__(*args, **kwargs)
17-
self._registry = OrderedDict(
18+
self.__error_registry = OrderedDict(
1819
(v.code, v) for _, v in self.__dict__.items() if is_error_class(v)
1920
)
21+
self.__subcatalog_registry = OrderedDict(
22+
(v.__name__, v) for _, v in self.__dict__.items() if isinstance(v, ErrorCatalogMeta)
23+
)
2024

2125
def __str__(self) -> str:
2226
return self.__name__
@@ -26,42 +30,69 @@ def __repr__(self) -> str:
2630

2731
def __iter__(self) -> t.Iterator[ExceptionWithCodeType]:
2832
"""Iterate over registered errors."""
29-
yield from self._registry.values()
33+
yield from self.__error_registry.values()
34+
yield from (e for c in self.__subcatalog_registry.values() for e in c)
3035

3136
def __len__(self) -> int:
32-
return len(self._registry)
37+
return len(self.all())
3338

3439
def __contains__(self, item: ExceptionWithCodeType) -> bool:
35-
return item in self._registry.values()
40+
return item in self.all()
3641

3742
def add_instance(self, error_class: ExceptionWithCodeType) -> None:
3843
"""Registers an ExceptionWithCode subtype as an element of the ErrorCatalog."""
39-
self._registry[error_class.code] = error_class
44+
self.__error_registry[error_class.code] = error_class
4045
setattr(self, error_class.code, error_class)
4146
error_class.catalog = t.cast("ErrorCatalog", self)
4247

4348
def all(self) -> t.Tuple[ExceptionWithCodeType, ...]:
44-
return tuple(self._registry.values())
49+
return tuple(self.__iter__())
50+
51+
def subcatalogs(self) -> t.Tuple["ErrorCatalogMeta", ...]:
52+
return tuple(self.__subcatalog_registry.values())
4553

4654

4755
class ErrorCatalog(metaclass=ErrorCatalogMeta):
4856
"""
4957
A class that can serve as a collection of named BaseErrors, gathered with a common reason.
5058
Instances of BaseErrors are meant to be declared as fields. Names of their fields may be
51-
used as default value of `code` for each instance. The catalog may set default value of
52-
`area` for all of them.
59+
used as default value of `code` for each instance.
60+
61+
>>> class SomeCatalog(ErrorCatalog):
62+
... SomeError = error_builder()
63+
... AnotherError = error_builder("ExpliciteNameForAnotherError")
64+
5365
5466
Developers are encouraged to gather errors of their business logic into such error classes.
67+
Such catalogs can be nested to structurize their relation further, in a form of a tree.
68+
69+
>>> class ExternalCatalog(ErrorCatalog):
70+
... ExternalError = error_builder()
71+
72+
>>> class CompositeCatalog(ErrorCatalog):
73+
... OwnError = error_builder()
74+
... ExternalCatalogIncluded = ExternalCatalog
75+
...
76+
... class NestedCatalog(ErrorCatalog):
77+
... NestedError = error_builder()
78+
79+
>>> assert CompositeCatalog.all() == (
80+
... CompositeCatalog.OwnError,
81+
... ExternalCatalog.ExternalError,
82+
... CompositeCatalog.NestedCatalog.NestedError
83+
... )
84+
85+
5586
If you want to reuse an error already attached to a catalog, use error's `clone` method
5687
like this:
5788
5889
>>> class OldCatalog(ErrorCatalog):
59-
... ERROR = ExceptionWithCode()
90+
... OldError = error_builder()
6091
6192
>>> class NewCatalog(ErrorCatalog):
62-
... AN_EXISTING_ERROR = OldCatalog.ERROR.clone()
93+
... AnExistingError = OldCatalog.OldError.clone()
6394
64-
>>> assert OldCatalog.ERROR == NewCatalog.AN_EXISTING_ERROR
65-
>>> assert OldCatalog.ERROR.catalog == OldCatalog
66-
>>> assert NewCatalog.AN_EXISTING_ERROR.catalog == NewCatalog
95+
>>> assert OldCatalog.OldCatalog is not NewCatalog.AnExistingError
96+
>>> assert OldCatalog.OldCatalog.catalog == OldCatalog
97+
>>> assert NewCatalog.AnExistingError.catalog == NewCatalog
6798
"""

tests/test_nested_catalog.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import typing as t
2+
3+
from pca.packages.errors import (
4+
ErrorCatalog,
5+
ExceptionWithCode,
6+
error_builder,
7+
)
8+
9+
10+
class ExternalCatalog(ErrorCatalog):
11+
ExternalError: t.Type[ExceptionWithCode] = error_builder()
12+
13+
14+
class CompositeCatalog(ErrorCatalog):
15+
OwnError: t.Type[ExceptionWithCode] = error_builder()
16+
ExternalCatalog = ExternalCatalog
17+
18+
class NestedCatalog(ErrorCatalog):
19+
NestedError: t.Type[ExceptionWithCode] = error_builder()
20+
21+
22+
def test_subcatalogs() -> None:
23+
assert ExternalCatalog.subcatalogs() == ()
24+
assert CompositeCatalog.subcatalogs() == (ExternalCatalog, CompositeCatalog.NestedCatalog)
25+
26+
27+
def test_repr() -> None:
28+
assert repr(CompositeCatalog.NestedCatalog) == (
29+
"test_nested_catalog.CompositeCatalog.NestedCatalog"
30+
)
31+
assert repr(CompositeCatalog.ExternalCatalog) == "test_nested_catalog.ExternalCatalog"
32+
33+
34+
def test_all() -> None:
35+
assert CompositeCatalog.all() == (
36+
CompositeCatalog.OwnError,
37+
ExternalCatalog.ExternalError,
38+
CompositeCatalog.NestedCatalog.NestedError,
39+
)
40+
41+
42+
def test_len() -> None:
43+
assert len(CompositeCatalog.NestedCatalog) == 1
44+
assert len(CompositeCatalog) == 3
45+
46+
47+
def test_contains() -> None:
48+
assert CompositeCatalog.NestedCatalog.NestedError in CompositeCatalog
49+
assert ExternalCatalog.ExternalError in CompositeCatalog

0 commit comments

Comments
 (0)