Skip to content

Commit acbff0e

Browse files
author
Sergio García Prado
authored
Merge pull request #308 from minos-framework/issue-150-refactor-broker-builder-classes
#150 - Improve `BrokerSubscriberBuilder`
2 parents 1ff0f9d + 0ee393c commit acbff0e

File tree

42 files changed

+1058
-249
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1058
-249
lines changed

packages/core/minos-microservice-common/minos/common/builders.py

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
from abc import (
66
ABC,
7-
abstractmethod,
87
)
98
from typing import (
109
Any,
1110
Generic,
11+
Optional,
1212
TypeVar,
13+
Union,
14+
get_args,
1315
)
1416

1517
from .config import (
@@ -25,25 +27,50 @@
2527
class Builder(SetupMixin, ABC, Generic[Instance]):
2628
"""Builder class."""
2729

28-
def __init__(self, *args, **kwargs):
30+
def __init__(self, instance_cls: Optional[type[Instance]] = None, *args, **kwargs):
31+
2932
super().__init__(*args, **kwargs)
33+
if instance_cls is None:
34+
instance_cls = self._get_cls()
35+
3036
self.kwargs = dict()
37+
self.instance_cls = instance_cls
38+
39+
def _get_cls(self) -> Optional[type]:
40+
# noinspection PyUnresolvedReferences
41+
bases = self.__orig_bases__
42+
43+
instance_cls = get_args(next((base for base in bases if len(get_args(base))), None))[0]
44+
45+
if not isinstance(instance_cls, type):
46+
return None
47+
48+
return instance_cls
3149

3250
def copy(self: type[B]) -> B:
3351
"""Get a copy of the instance.
3452
35-
:return: A ``BrokerSubscriberBuilder`` instance.
53+
:return: A ``Builder`` instance.
3654
"""
37-
return self.new().with_kwargs(self.kwargs)
55+
return self.new().with_cls(self.instance_cls).with_kwargs(self.kwargs)
3856

3957
@classmethod
4058
def new(cls: type[B]) -> B:
4159
"""Get a new instance.
4260
43-
:return: A ``BrokerSubscriberBuilder`` instance.
61+
:return: A ``Builder`` instance.
4462
"""
4563
return cls()
4664

65+
def with_cls(self: B, cls: type) -> B:
66+
"""Set class to be built.
67+
68+
:param cls: The class to be set.
69+
:return: This method return the builder instance.
70+
"""
71+
self.instance_cls = cls
72+
return self
73+
4774
def with_kwargs(self: B, kwargs: dict[str, Any]) -> B:
4875
"""Set kwargs.
4976
@@ -62,12 +89,18 @@ def with_config(self: B, config: Config) -> B:
6289
"""
6390
return self
6491

65-
@abstractmethod
6692
def build(self) -> Instance:
6793
"""Build the instance.
6894
69-
:return: A ``BrokerSubscriber`` instance.
95+
:return: A ``Instance`` instance.
7096
"""
97+
return self.instance_cls(**self.kwargs)
98+
99+
def __eq__(self, other: Any) -> bool:
100+
return isinstance(other, type(self)) and self.instance_cls == other.instance_cls and self.kwargs == other.kwargs
101+
102+
def __repr__(self) -> str:
103+
return f"{type(self).__name__}({self.instance_cls.__name__}, {self.kwargs!r})"
71104

72105

73106
Ins = TypeVar("Ins", bound="BuildableMixin")
@@ -76,28 +109,38 @@ def build(self) -> Instance:
76109
class BuildableMixin(SetupMixin):
77110
"""Buildable Mixin class."""
78111

79-
_builder_cls: type[Builder[Ins]]
112+
_builder: Union[Builder[Ins], type[Builder[Ins]]] = Builder
80113

81114
@classmethod
82-
def _from_config(cls: type[Ins], config: Config, **kwargs) -> Ins:
83-
return cls.get_builder().new().with_config(config).with_kwargs(kwargs).build()
115+
def _from_config(cls, config: Config, **kwargs):
116+
return cls.get_builder().with_config(config).with_kwargs(kwargs).build()
84117

85118
@classmethod
86-
def set_builder(cls: type[Ins], builder: type[Builder[Ins]]) -> None:
119+
def set_builder(cls: type[Ins], builder: Union[Builder[Ins], type[Builder[Ins]]]) -> None:
87120
"""Set a builder class.
88121
89122
:param builder: The builder class to be set.
90123
:return: This method does not return anything.
91124
"""
92-
cls._builder_cls = builder
125+
if not isinstance(builder, Builder) and not (isinstance(builder, type) and issubclass(builder, Builder)):
126+
raise ValueError(f"Given builder value is invalid: {builder!r}")
127+
128+
cls._builder = builder
93129

94130
@classmethod
95-
def get_builder(cls) -> type[Builder[Ins]]:
131+
def get_builder(cls) -> Builder[Ins]:
96132
"""Get the builder class.
97133
98-
:return: A ``Builder`` subclass.
134+
:return: A ``Builder`` instance.
99135
"""
100-
return cls._builder_cls
136+
builder = cls._builder
137+
138+
if isinstance(builder, Builder):
139+
builder = builder.copy()
140+
else:
141+
builder = builder.new()
142+
143+
return builder.with_cls(cls)
101144

102145

103146
B = TypeVar("B", bound=Builder)

packages/core/minos-microservice-common/minos/common/config/v2.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import (
1212
TYPE_CHECKING,
1313
Any,
14+
Union,
1415
)
1516

1617
from ..exceptions import (
@@ -39,7 +40,7 @@ def _version(self) -> int:
3940
def _get_name(self) -> str:
4041
return self.get_by_key("name")
4142

42-
def _get_injections(self) -> list[type[InjectableMixin]]:
43+
def _get_injections(self) -> list[Union[InjectableMixin, type[InjectableMixin]]]:
4344
from ..builders import (
4445
BuildableMixin,
4546
)
@@ -82,15 +83,15 @@ def _get_injections(self) -> list[type[InjectableMixin]]:
8283
if (
8384
not issubclass(type_, InjectableMixin)
8485
and issubclass(type_, BuildableMixin)
85-
and issubclass((builder_type := type_.get_builder()), InjectableMixin)
86+
and isinstance((builder_type := type_.get_builder()), InjectableMixin)
8687
):
8788
type_ = builder_type
88-
89-
if not issubclass(type_, InjectableMixin):
89+
elif not issubclass(type_, InjectableMixin):
9090
raise MinosConfigException(f"{type_!r} must be subclass of {InjectableMixin!r}.")
9191

9292
ans.append(type_)
9393

94+
# noinspection PyTypeChecker
9495
return ans
9596

9697
def _get_databases(self) -> dict[str, dict[str, Any]]:
Lines changed: 117 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,152 @@
11
import unittest
2-
from abc import (
3-
ABC,
4-
)
52
from typing import (
6-
Any,
3+
Generic,
4+
TypeVar,
75
)
86

97
from minos.common import (
8+
BuildableMixin,
109
Builder,
1110
Config,
12-
SetupMixin,
1311
)
1412
from tests.utils import (
1513
CONFIG_FILE_PATH,
1614
)
1715

1816

19-
class _Builder(Builder[dict[str, Any]]):
20-
def build(self) -> dict[str, Any]:
21-
"""For testing purposes."""
22-
return self.kwargs
23-
24-
2517
class TestBuilder(unittest.TestCase):
26-
def test_abstract(self):
27-
self.assertTrue(issubclass(Builder, (ABC, SetupMixin)))
28-
# noinspection PyUnresolvedReferences
29-
self.assertEqual({"build"}, Builder.__abstractmethods__)
30-
3118
def test_new(self):
32-
builder = _Builder.new()
33-
self.assertIsInstance(builder, _Builder)
19+
builder = Builder.new().with_cls(dict)
20+
self.assertIsInstance(builder, Builder)
3421
self.assertEqual(dict(), builder.kwargs)
3522

3623
def test_copy(self):
37-
base = _Builder.new().with_kwargs({"one": "two"})
24+
base = Builder.new().with_cls(dict).with_kwargs({"one": "two"})
3825
builder = base.copy()
3926
self.assertNotEqual(id(base), id(builder))
40-
self.assertIsInstance(builder, _Builder)
27+
self.assertIsInstance(builder, Builder)
4128
self.assertEqual({"one": "two"}, builder.kwargs)
29+
self.assertEqual(dict, builder.instance_cls)
4230

4331
def test_with_kwargs(self):
44-
builder = _Builder().with_kwargs({"foo": "bar"})
45-
self.assertIsInstance(builder, _Builder)
32+
builder = Builder().with_cls(dict).with_kwargs({"foo": "bar"})
33+
self.assertIsInstance(builder, Builder)
4634
self.assertEqual({"foo": "bar"}, builder.kwargs)
4735

4836
def test_with_config(self):
4937
config = Config(CONFIG_FILE_PATH)
50-
builder = _Builder().with_config(config)
51-
self.assertIsInstance(builder, _Builder)
38+
builder = Builder().with_cls(dict).with_config(config)
39+
self.assertIsInstance(builder, Builder)
5240
self.assertEqual(dict(), builder.kwargs)
5341

5442
def test_build(self):
55-
builder = _Builder().with_kwargs({"one": "two"})
56-
self.assertIsInstance(builder, _Builder)
43+
builder = Builder().with_cls(dict).with_kwargs({"one": "two"})
44+
self.assertIsInstance(builder, Builder)
5745
self.assertEqual({"one": "two"}, builder.build())
5846

47+
def test_str(self):
48+
builder = Builder().with_cls(dict).with_kwargs({"one": "two"})
49+
50+
self.assertEqual("Builder(dict, {'one': 'two'})", repr(builder))
51+
52+
def test_cmp(self):
53+
base = Builder().with_cls(dict).with_kwargs({"one": "two"})
54+
55+
one = Builder().with_cls(dict).with_kwargs({"one": "two"})
56+
self.assertEqual(base, one)
57+
58+
two = Builder().with_cls(int).with_kwargs({"one": "two"})
59+
self.assertNotEqual(base, two)
60+
61+
three = Builder().with_cls(dict).with_kwargs({"three": "four"})
62+
self.assertNotEqual(base, three)
63+
64+
def test_instance_cls_from_generic(self):
65+
class _Builder(Builder):
66+
"""For Testing purposes."""
67+
68+
class _Builder2(Builder[int]):
69+
"""For Testing purposes."""
70+
71+
class _Builder3(_Builder2):
72+
"""For Testing purposes."""
73+
74+
T = TypeVar("T")
75+
76+
class _Builder4(_Builder2, Generic[T]):
77+
"""For Testing purposes."""
78+
79+
class _Builder5(_Builder4[float]):
80+
"""For Testing purposes."""
81+
82+
self.assertEqual(None, _Builder().instance_cls)
83+
self.assertEqual(int, _Builder2().instance_cls)
84+
self.assertEqual(int, _Builder3().instance_cls)
85+
self.assertEqual(None, _Builder4().instance_cls)
86+
self.assertEqual(float, _Builder5().instance_cls)
87+
88+
89+
class TestBuildableMixin(unittest.TestCase):
90+
def test_get_builder_default(self):
91+
class _Foo(BuildableMixin):
92+
"""For Testing purposes."""
93+
94+
self.assertEqual(Builder().with_cls(_Foo), _Foo.get_builder())
95+
96+
def test_get_builder_custom_type(self):
97+
class _Foo(BuildableMixin):
98+
"""For Testing purposes."""
99+
100+
class _Builder(Builder):
101+
"""For Testing purposes."""
102+
103+
_Foo.set_builder(_Builder)
104+
105+
self.assertEqual(_Builder().with_cls(_Foo), _Foo.get_builder())
106+
107+
def test_get_builder_custom_instance(self):
108+
class _Foo(BuildableMixin):
109+
"""For Testing purposes."""
110+
111+
class _Builder(Builder):
112+
"""For Testing purposes."""
113+
114+
_Foo.set_builder(_Builder().with_kwargs({"foo": "bar"}))
115+
116+
self.assertEqual(_Builder().with_cls(_Foo).with_kwargs({"foo": "bar"}), _Foo.get_builder())
117+
118+
def test_set_builder_raises(self):
119+
class _Foo(BuildableMixin):
120+
"""For Testing purposes."""
121+
122+
with self.assertRaises(ValueError):
123+
# noinspection PyTypeChecker
124+
_Foo.set_builder(int)
125+
126+
def test_from_config(self):
127+
class _Foo(BuildableMixin):
128+
"""For Testing purposes."""
129+
130+
# noinspection PyShadowingNames
131+
def __init__(self, config):
132+
super().__init__()
133+
self.config = config
134+
135+
class _Builder(Builder):
136+
"""For Testing purposes."""
137+
138+
def with_config(self, config: Config):
139+
"""For Testing purposes."""
140+
self.kwargs["config"] = config
141+
return super().with_config(config)
142+
143+
_Foo.set_builder(_Builder)
144+
145+
config = Config(CONFIG_FILE_PATH)
146+
foo = _Foo.from_config(config)
147+
148+
self.assertEqual(config, foo.config)
149+
59150

60151
if __name__ == "__main__":
61152
unittest.main()

packages/core/minos-microservice-common/tests/test_common/test_config/test_v2/test_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def test_injections(self):
6565
FakeBrokerClientPool,
6666
FakeHttpConnector,
6767
FakeBrokerPublisher,
68-
FakeBrokerSubscriberBuilder,
68+
FakeBrokerSubscriberBuilder(FakeBrokerSubscriber),
6969
FakeEventRepository,
7070
FakeSnapshotRepository,
7171
FakeTransactionRepository,

packages/core/minos-microservice-common/tests/utils.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,6 @@ class FakeBrokerPublisher(BuildableMixin):
132132
class FakeBrokerPublisherBuilder(Builder[FakeBrokerPublisher]):
133133
"""For testing purposes."""
134134

135-
def build(self) -> FakeBrokerPublisher:
136-
return FakeBrokerPublisher()
137-
138135

139136
FakeBrokerPublisher.set_builder(FakeBrokerPublisherBuilder)
140137

@@ -147,9 +144,6 @@ class FakeBrokerSubscriber(BuildableMixin):
147144
class FakeBrokerSubscriberBuilder(Builder[FakeBrokerSubscriber]):
148145
"""For testing purposes."""
149146

150-
def build(self) -> FakeBrokerSubscriber:
151-
return FakeBrokerSubscriber()
152-
153147

154148
FakeBrokerSubscriber.set_builder(FakeBrokerSubscriberBuilder)
155149

0 commit comments

Comments
 (0)