From 0e14df90666ba818cd3dbf007061a15b9bb235bb Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Fri, 27 Nov 2020 16:01:34 -0500 Subject: [PATCH 01/13] Don't need to repeat the Python version check info. --- tests/dataclasses/integration/conftest.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/dataclasses/integration/conftest.py b/tests/dataclasses/integration/conftest.py index 02a740b..7ebcb78 100644 --- a/tests/dataclasses/integration/conftest.py +++ b/tests/dataclasses/integration/conftest.py @@ -3,9 +3,6 @@ import pytest -if sys.version_info < (3, 7): # pragma: no cover - collect_ignore_glob = ['*.py'] - @pytest.fixture(scope="session", autouse=True) def docs_path(): From 0d7dc55f3a066d22c40c7dc0b61e96b2c23a16bf Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Fri, 27 Nov 2020 17:51:14 -0500 Subject: [PATCH 02/13] First stab at factory decorator with __wired_factory__ support. --- examples/decorators/basic_class.py | 23 +++++++ examples/decorators/no_decorator.py | 22 +++++++ examples/decorators/no_decorator_class.py | 18 ++++++ .../decorators/wired_factory_classmethod.py | 24 ++++++++ src/wired/__init__.py | 3 +- src/wired/decorators.py | 61 +++++++++++++++++++ tests/conftest.py | 13 ++++ tests/decorators/__init__.py | 0 tests/decorators/conftest.py | 23 +++++++ tests/decorators/test_basic_class.py | 19 ++++++ tests/decorators/test_no_decorator.py | 27 ++++++++ tests/decorators/test_no_decorator_class.py | 25 ++++++++ .../test_wired_factory_classmethod.py | 14 +++++ 13 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 examples/decorators/basic_class.py create mode 100644 examples/decorators/no_decorator.py create mode 100644 examples/decorators/no_decorator_class.py create mode 100644 examples/decorators/wired_factory_classmethod.py create mode 100644 src/wired/decorators.py create mode 100644 tests/conftest.py create mode 100644 tests/decorators/__init__.py create mode 100644 tests/decorators/conftest.py create mode 100644 tests/decorators/test_basic_class.py create mode 100644 tests/decorators/test_no_decorator.py create mode 100644 tests/decorators/test_no_decorator_class.py create mode 100644 tests/decorators/test_wired_factory_classmethod.py diff --git a/examples/decorators/basic_class.py b/examples/decorators/basic_class.py new file mode 100644 index 0000000..6076fb6 --- /dev/null +++ b/examples/decorators/basic_class.py @@ -0,0 +1,23 @@ +""" +Simplest example for ``@factory`` decorator: a basic class. +""" +from wired import ServiceContainer +from wired import factory + + +class Greeter: + def __init__(self, name): + self.name = name + + +@factory() +class Greeting: + def __init__(self, container: ServiceContainer): + self.greeter = container.get(Greeter) + + def greet(self): + return f'Hello from {self.greeter.name}' + + +def greeter_factory(container): + return Greeter('Marie') diff --git a/examples/decorators/no_decorator.py b/examples/decorators/no_decorator.py new file mode 100644 index 0000000..0be411c --- /dev/null +++ b/examples/decorators/no_decorator.py @@ -0,0 +1,22 @@ +from wired import ServiceContainer + + +class Greeter: + def __init__(self, name): + self.name = name + + +class Greeting: + def __init__(self, container: ServiceContainer): + self.greeter = container.get(Greeter) + + def greet(self): + return f'Hello from {self.greeter.name}' + + +def greeter_factory(container): + return Greeter('Marie') + + +def greeting_factory(container): + return Greeting(container) diff --git a/examples/decorators/no_decorator_class.py b/examples/decorators/no_decorator_class.py new file mode 100644 index 0000000..e069ab5 --- /dev/null +++ b/examples/decorators/no_decorator_class.py @@ -0,0 +1,18 @@ +from wired import ServiceContainer + + +class Greeter: + def __init__(self, name): + self.name = name + + +class Greeting: + def __init__(self, container: ServiceContainer): + self.greeter = container.get(Greeter) + + def greet(self): + return f'Hello from {self.greeter.name}' + + +def greeter_factory(container): + return Greeter('Marie') diff --git a/examples/decorators/wired_factory_classmethod.py b/examples/decorators/wired_factory_classmethod.py new file mode 100644 index 0000000..f3d3b7a --- /dev/null +++ b/examples/decorators/wired_factory_classmethod.py @@ -0,0 +1,24 @@ +""" +Simple usage of a ``_wired_factory__`` classmethod. +""" +from wired import ServiceContainer +from wired import factory + + +@factory() +class Greeter: + def __init__(self, name): + self.name = name + + @classmethod + def __wired_factory__(cls, container: ServiceContainer): + return Greeter('Marie') + + +@factory() +class Greeting: + def __init__(self, container: ServiceContainer): + self.greeter = container.get(Greeter) + + def greet(self): + return f'Hello from {self.greeter.name}' diff --git a/src/wired/__init__.py b/src/wired/__init__.py index bb6fd40..a650a36 100644 --- a/src/wired/__init__.py +++ b/src/wired/__init__.py @@ -1,4 +1,5 @@ -__all__ = ['ServiceContainer', 'ServiceRegistry'] +__all__ = ['ServiceContainer', 'ServiceRegistry', 'factory'] from .container import ServiceContainer from .container import ServiceRegistry +from .decorators import factory diff --git a/src/wired/decorators.py b/src/wired/decorators.py new file mode 100644 index 0000000..9fde4c9 --- /dev/null +++ b/src/wired/decorators.py @@ -0,0 +1,61 @@ +from venusian import attach, Scanner + +from wired import ServiceRegistry + + +# noinspection PyPep8Naming +class factory: + """ + Register a factory for a class that can sniff dependencies. + + The factory will be registered with a :class:`wired.ServiceRegistry` when + performing a venusian scan. + + .. code-block:: python + + from sqlalchemy.orm import Session + + @factory + @dataclass + class LoginService: + db: Session + + # ... later + + registry = ServiceRegistry() + scanner = venusian.Scanner(registry=registry) + scanner.scan() + + # ... later + + container = registry.create_container() + svc = container.get(LoginService) + + .. seealso:: + + - :func:`wired.ServiceRegistry.register_factory` + + """ + + def __init__(self, for_=None, context=None, name: str = ''): + self.for_ = for_ + self.context = context + self.name = name + + def __call__(self, wrapped): + def callback(scanner: Scanner, name: str, cls): + registry: ServiceRegistry = getattr(scanner, 'registry') + # If there is a for_ use it, otherwise, register for the same + # class as the instance + for_ = self.for_ if self.for_ else cls + + def _default_factory(container): + return cls(container) + + _factory = getattr(cls, '__wired_factory__', _default_factory) + registry.register_factory( + _factory, for_, context=self.context, name=self.name + ) + + attach(wrapped, callback, category='wired') + return wrapped diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f665893 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +import os +import sys + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def examples_path(): + """ Automatically add the root of the repo to path """ + tutorial_path = os.path.abspath( + os.path.join(os.path.dirname(__file__), '..') + ) + sys.path.insert(0, tutorial_path) diff --git a/tests/decorators/__init__.py b/tests/decorators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/decorators/conftest.py b/tests/decorators/conftest.py new file mode 100644 index 0000000..8fd9a09 --- /dev/null +++ b/tests/decorators/conftest.py @@ -0,0 +1,23 @@ +import pytest +from venusian import Scanner + +from wired import ServiceRegistry + + +@pytest.fixture +def scannable(): + """ Each test file needs to implement this """ + raise NotImplementedError() + + +@pytest.fixture +def registry(): + return ServiceRegistry() + + +@pytest.fixture +def container(registry, scannable): + s = Scanner(registry=registry) + s.scan(scannable) + c = registry.create_container() + return c diff --git a/tests/decorators/test_basic_class.py b/tests/decorators/test_basic_class.py new file mode 100644 index 0000000..0b63494 --- /dev/null +++ b/tests/decorators/test_basic_class.py @@ -0,0 +1,19 @@ +import pytest + +from examples.decorators import basic_class + + +@pytest.fixture +def scannable(): + return basic_class + + +def test_greeter(container, registry): + from examples.decorators.basic_class import ( + greeter_factory, + Greeter, + Greeting, + ) + registry.register_factory(greeter_factory, Greeter) + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/test_no_decorator.py b/tests/decorators/test_no_decorator.py new file mode 100644 index 0000000..ec7d66a --- /dev/null +++ b/tests/decorators/test_no_decorator.py @@ -0,0 +1,27 @@ +""" +Starting-point example that uses no decorator nor +``wired_factory`` support but the ``Greeting`` class + serves as its factory. + +""" +import pytest + +from examples.decorators import no_decorator + + +@pytest.fixture +def scannable(): + return no_decorator + + +def test_greeter(container, registry): + from examples.decorators.no_decorator import ( + greeter_factory, + Greeter, + greeting_factory, + Greeting, + ) + registry.register_factory(greeter_factory, Greeter) + registry.register_factory(greeting_factory, Greeting) + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/test_no_decorator_class.py b/tests/decorators/test_no_decorator_class.py new file mode 100644 index 0000000..d27982e --- /dev/null +++ b/tests/decorators/test_no_decorator_class.py @@ -0,0 +1,25 @@ +""" +Starting-point example that uses no decorator nor +``wired_factory`` support. + +""" +import pytest + +from examples.decorators import no_decorator_class + + +@pytest.fixture +def scannable(): + return no_decorator_class + + +def test_greeter(container, registry): + from examples.decorators.no_decorator_class import ( + greeter_factory, + Greeter, + Greeting, + ) + registry.register_factory(greeter_factory, Greeter) + registry.register_factory(Greeting, Greeting) + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/test_wired_factory_classmethod.py b/tests/decorators/test_wired_factory_classmethod.py new file mode 100644 index 0000000..ba6155f --- /dev/null +++ b/tests/decorators/test_wired_factory_classmethod.py @@ -0,0 +1,14 @@ +import pytest + +from examples.decorators import wired_factory_classmethod + + +@pytest.fixture +def scannable(): + return wired_factory_classmethod + + +def test_greeter(container, registry): + from examples.decorators.wired_factory_classmethod import Greeting + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() From fdcd23708e6ee84daa791affae04e8f2bd303ad0 Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Fri, 27 Nov 2020 18:56:06 -0500 Subject: [PATCH 03/13] Improve coverage on docs/tutorials --- docs/tutorial/context/app.py | 4 ---- docs/tutorial/datastore/__init__.py | 4 ---- docs/tutorial/decoupled/__init__.py | 4 ---- docs/tutorial/factory/app.py | 4 ---- docs/tutorial/overrides/__init__.py | 10 +++------- docs/tutorial/requests_views/__init__.py | 4 ---- docs/tutorial/settings/app.py | 4 ---- docs/tutorial/simple/app.py | 4 ---- tests/tutorial/test_context.py | 6 ++++++ tests/tutorial/test_datastore.py | 5 +++++ tests/tutorial/test_decoupled.py | 6 ++++++ tests/tutorial/test_factory.py | 6 ++++++ tests/tutorial/test_overrides.py | 5 +++++ tests/tutorial/test_requests_views.py | 6 ++++++ tests/tutorial/test_settings.py | 6 ++++++ tests/tutorial/test_simple.py | 6 ++++++ 16 files changed, 49 insertions(+), 35 deletions(-) diff --git a/docs/tutorial/context/app.py b/docs/tutorial/context/app.py index 6089c4f..1649638 100644 --- a/docs/tutorial/context/app.py +++ b/docs/tutorial/context/app.py @@ -111,7 +111,3 @@ def main(): # then test the result. french_customer = FrenchCustomer(name='Henri') assert 'Bonjour Henri !!' == greet_customer(registry, french_customer) - - -if __name__ == '__main__': - main() diff --git a/docs/tutorial/datastore/__init__.py b/docs/tutorial/datastore/__init__.py index 0f900e4..2478021 100644 --- a/docs/tutorial/datastore/__init__.py +++ b/docs/tutorial/datastore/__init__.py @@ -122,7 +122,3 @@ def main(): registry = app_bootstrap(settings) greetings = sample_interactions(registry) assert greetings == ['Hello Mary !!', 'Bonjour Henri !!'] - - -if __name__ == '__main__': - main() diff --git a/docs/tutorial/decoupled/__init__.py b/docs/tutorial/decoupled/__init__.py index a0239f0..f81413d 100644 --- a/docs/tutorial/decoupled/__init__.py +++ b/docs/tutorial/decoupled/__init__.py @@ -94,7 +94,3 @@ def main(): french_customer = FrenchCustomer(name='Henri') assert 'Bonjour Henri !!' == greet_customer(registry, french_customer) - - -if __name__ == '__main__': - main() diff --git a/docs/tutorial/factory/app.py b/docs/tutorial/factory/app.py index f1bee06..a57b2af 100644 --- a/docs/tutorial/factory/app.py +++ b/docs/tutorial/factory/app.py @@ -57,7 +57,3 @@ def main(): registry = setup() greeting = greet_a_customer(registry) assert greeting == 'Hello !!' - - -if __name__ == '__main__': - main() diff --git a/docs/tutorial/overrides/__init__.py b/docs/tutorial/overrides/__init__.py index 7933814..b237ea6 100644 --- a/docs/tutorial/overrides/__init__.py +++ b/docs/tutorial/overrides/__init__.py @@ -36,7 +36,7 @@ def setup(registry: ServiceRegistry, settings: Settings): # Make the greeter factory, using punctuation from settings punctuation = settings.punctuation - def default_greeter_factory(container) -> Greeter: + def default_greeter_factory(container) -> Greeter: # pragma: no cover # Use the dataclass default for greeting return Greeter(punctuation=punctuation) @@ -64,7 +64,7 @@ def app_bootstrap(settings: Settings) -> ServiceRegistry: def customer_interaction( - container: ServiceContainer, customer: Customer + container: ServiceContainer, customer: Customer ) -> str: """ Customer comes in, handle the steps in greeting them """ @@ -97,8 +97,4 @@ def main(): settings = Settings(punctuation='!!') registry = app_bootstrap(settings) greetings = sample_interactions(registry) - assert greetings == ['Hello Mary !!', 'Bonjour Henri !!'] - - -if __name__ == '__main__': - main() + assert ['Override Mary !!', 'Bonjour Henri !!'] == greetings diff --git a/docs/tutorial/requests_views/__init__.py b/docs/tutorial/requests_views/__init__.py index 8fcc8a8..925edf2 100644 --- a/docs/tutorial/requests_views/__init__.py +++ b/docs/tutorial/requests_views/__init__.py @@ -128,7 +128,3 @@ def main(): registry = app_bootstrap(settings) greetings = sample_interactions(registry) assert greetings == ['Hello Mary !!', 'Bonjour Henri !!'] - - -if __name__ == '__main__': - main() diff --git a/docs/tutorial/settings/app.py b/docs/tutorial/settings/app.py index dab6f8c..3bb2f82 100644 --- a/docs/tutorial/settings/app.py +++ b/docs/tutorial/settings/app.py @@ -70,7 +70,3 @@ def main(): registry = setup(settings) greeting = greet_a_customer(registry) assert greeting == 'Hello !!' - - -if __name__ == '__main__': - main() diff --git a/docs/tutorial/simple/app.py b/docs/tutorial/simple/app.py index f4795e2..9899d85 100644 --- a/docs/tutorial/simple/app.py +++ b/docs/tutorial/simple/app.py @@ -56,7 +56,3 @@ def main(): registry = setup() greeting = greet_a_customer(registry) assert greeting == 'Hello !!' - - -if __name__ == '__main__': - main() diff --git a/tests/tutorial/test_context.py b/tests/tutorial/test_context.py index 4076a37..8943d6a 100644 --- a/tests/tutorial/test_context.py +++ b/tests/tutorial/test_context.py @@ -45,3 +45,9 @@ def test_greet_french_customer(registry, french_customer): actual = greet_customer(registry, french_customer) assert 'Bonjour Henri !!' == actual + + +def test_main(): + from tutorial.context.app import main + + assert None is main() diff --git a/tests/tutorial/test_datastore.py b/tests/tutorial/test_datastore.py index e63caac..50e09f0 100644 --- a/tests/tutorial/test_datastore.py +++ b/tests/tutorial/test_datastore.py @@ -25,3 +25,8 @@ def test_sample_interactions(registry): greetings = sample_interactions(registry) assert 'Hello Mary !!' == greetings[0] assert 'Bonjour Henri !!' == greetings[1] + + +def test_main(): + from tutorial.datastore import main + assert None is main() diff --git a/tests/tutorial/test_decoupled.py b/tests/tutorial/test_decoupled.py index 81e9d6c..5960465 100644 --- a/tests/tutorial/test_decoupled.py +++ b/tests/tutorial/test_decoupled.py @@ -45,3 +45,9 @@ def test_greet_french_customer(registry, french_customer): actual = greet_customer(registry, french_customer) assert 'Bonjour Henri !!' == actual + + +def test_main(): + from tutorial.decoupled import main + + assert None is main() diff --git a/tests/tutorial/test_factory.py b/tests/tutorial/test_factory.py index e36f0d5..4fcdc6b 100644 --- a/tests/tutorial/test_factory.py +++ b/tests/tutorial/test_factory.py @@ -16,3 +16,9 @@ def test_greet_a_customer(registry): actual = greet_a_customer(registry) assert 'Hello !!' == actual + + +def test_main(): + from tutorial.factory.app import main + + assert None is main() diff --git a/tests/tutorial/test_overrides.py b/tests/tutorial/test_overrides.py index 6412fe2..e77c438 100644 --- a/tests/tutorial/test_overrides.py +++ b/tests/tutorial/test_overrides.py @@ -25,3 +25,8 @@ def test_sample_interactions(registry): greetings = sample_interactions(registry) assert 'Override Mary !!' == greetings[0] assert 'Bonjour Henri !!' == greetings[1] + + +def test_main(): + from tutorial.overrides import main + assert None is main() diff --git a/tests/tutorial/test_requests_views.py b/tests/tutorial/test_requests_views.py index adfc35b..033aff5 100644 --- a/tests/tutorial/test_requests_views.py +++ b/tests/tutorial/test_requests_views.py @@ -25,3 +25,9 @@ def test_sample_interactions(registry): greetings = sample_interactions(registry) assert 'Hello Mary !!' == greetings[0] assert 'Bonjour Henri !!' == greetings[1] + + +def test_main(): + from tutorial.requests_views import main + + assert None is main() diff --git a/tests/tutorial/test_settings.py b/tests/tutorial/test_settings.py index d625ce5..b038d75 100644 --- a/tests/tutorial/test_settings.py +++ b/tests/tutorial/test_settings.py @@ -24,3 +24,9 @@ def test_greet_a_customer(registry): actual = greet_a_customer(registry) assert 'Hello !!' == actual + + +def test_main(): + from tutorial.settings.app import main + + assert None is main() diff --git a/tests/tutorial/test_simple.py b/tests/tutorial/test_simple.py index 6eb601f..2350369 100644 --- a/tests/tutorial/test_simple.py +++ b/tests/tutorial/test_simple.py @@ -16,3 +16,9 @@ def test_greet_a_customer(registry): actual = greet_a_customer(registry) assert 'Hello !!' == actual + + +def test_main(): + from tutorial.simple.app import main + + assert None is main() From 906e8e516d93dcb3317132b1758cee5a4a2d444e Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Fri, 27 Nov 2020 19:07:45 -0500 Subject: [PATCH 04/13] Get tox to work when venusian isn't installed. --- docs/tutorial/overrides/__init__.py | 1 - src/wired/dataclasses/__init__.py | 1 - src/wired/decorators.py | 14 ++++++++------ tests/decorators/conftest.py | 8 +++++++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/tutorial/overrides/__init__.py b/docs/tutorial/overrides/__init__.py index b237ea6..fd13bc6 100644 --- a/docs/tutorial/overrides/__init__.py +++ b/docs/tutorial/overrides/__init__.py @@ -59,7 +59,6 @@ def app_bootstrap(settings: Settings) -> ServiceRegistry: from .custom import setup as addon_setup addon_setup(registry, settings) - return registry diff --git a/src/wired/dataclasses/__init__.py b/src/wired/dataclasses/__init__.py index f4f1b9a..41d19c0 100644 --- a/src/wired/dataclasses/__init__.py +++ b/src/wired/dataclasses/__init__.py @@ -4,4 +4,3 @@ from .field_types import injected from .models import Context from .registration import register_dataclass - diff --git a/src/wired/decorators.py b/src/wired/decorators.py index 9fde4c9..9451fb6 100644 --- a/src/wired/decorators.py +++ b/src/wired/decorators.py @@ -1,6 +1,8 @@ -from venusian import attach, Scanner - -from wired import ServiceRegistry +# wired is usable without venusian +try: + import venusian +except ImportError: + venusian = None # noinspection PyPep8Naming @@ -43,8 +45,8 @@ def __init__(self, for_=None, context=None, name: str = ''): self.name = name def __call__(self, wrapped): - def callback(scanner: Scanner, name: str, cls): - registry: ServiceRegistry = getattr(scanner, 'registry') + def callback(scanner: venusian.Scanner, name: str, cls): + registry = getattr(scanner, 'registry') # If there is a for_ use it, otherwise, register for the same # class as the instance for_ = self.for_ if self.for_ else cls @@ -57,5 +59,5 @@ def _default_factory(container): _factory, for_, context=self.context, name=self.name ) - attach(wrapped, callback, category='wired') + venusian.attach(wrapped, callback, category='wired') return wrapped diff --git a/tests/decorators/conftest.py b/tests/decorators/conftest.py index 8fd9a09..dafceeb 100644 --- a/tests/decorators/conftest.py +++ b/tests/decorators/conftest.py @@ -1,8 +1,14 @@ +import sys + import pytest -from venusian import Scanner from wired import ServiceRegistry +if sys.version_info < (3, 7): # pragma: no cover + collect_ignore_glob = ['*.py'] +else: + from venusian import Scanner + @pytest.fixture def scannable(): From d5887a4f30ecb9e2cf2e2bdf5fa85660fef74c8e Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Fri, 27 Nov 2020 19:17:18 -0500 Subject: [PATCH 05/13] Get some black fixes. --- docs/tutorial/overrides/__init__.py | 2 +- tests/decorators/test_basic_class.py | 1 + tests/decorators/test_no_decorator.py | 1 + tests/decorators/test_no_decorator_class.py | 1 + tests/decorators/test_wired_factory_classmethod.py | 1 + tests/tutorial/test_datastore.py | 1 + tests/tutorial/test_overrides.py | 1 + 7 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/tutorial/overrides/__init__.py b/docs/tutorial/overrides/__init__.py index fd13bc6..a0545b1 100644 --- a/docs/tutorial/overrides/__init__.py +++ b/docs/tutorial/overrides/__init__.py @@ -63,7 +63,7 @@ def app_bootstrap(settings: Settings) -> ServiceRegistry: def customer_interaction( - container: ServiceContainer, customer: Customer + container: ServiceContainer, customer: Customer ) -> str: """ Customer comes in, handle the steps in greeting them """ diff --git a/tests/decorators/test_basic_class.py b/tests/decorators/test_basic_class.py index 0b63494..93f1249 100644 --- a/tests/decorators/test_basic_class.py +++ b/tests/decorators/test_basic_class.py @@ -14,6 +14,7 @@ def test_greeter(container, registry): Greeter, Greeting, ) + registry.register_factory(greeter_factory, Greeter) greeting: Greeting = container.get(Greeting) assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/test_no_decorator.py b/tests/decorators/test_no_decorator.py index ec7d66a..3fb2206 100644 --- a/tests/decorators/test_no_decorator.py +++ b/tests/decorators/test_no_decorator.py @@ -21,6 +21,7 @@ def test_greeter(container, registry): greeting_factory, Greeting, ) + registry.register_factory(greeter_factory, Greeter) registry.register_factory(greeting_factory, Greeting) greeting: Greeting = container.get(Greeting) diff --git a/tests/decorators/test_no_decorator_class.py b/tests/decorators/test_no_decorator_class.py index d27982e..3facde7 100644 --- a/tests/decorators/test_no_decorator_class.py +++ b/tests/decorators/test_no_decorator_class.py @@ -19,6 +19,7 @@ def test_greeter(container, registry): Greeter, Greeting, ) + registry.register_factory(greeter_factory, Greeter) registry.register_factory(Greeting, Greeting) greeting: Greeting = container.get(Greeting) diff --git a/tests/decorators/test_wired_factory_classmethod.py b/tests/decorators/test_wired_factory_classmethod.py index ba6155f..496ab82 100644 --- a/tests/decorators/test_wired_factory_classmethod.py +++ b/tests/decorators/test_wired_factory_classmethod.py @@ -10,5 +10,6 @@ def scannable(): def test_greeter(container, registry): from examples.decorators.wired_factory_classmethod import Greeting + greeting: Greeting = container.get(Greeting) assert 'Hello from Marie' == greeting.greet() diff --git a/tests/tutorial/test_datastore.py b/tests/tutorial/test_datastore.py index 50e09f0..e14154a 100644 --- a/tests/tutorial/test_datastore.py +++ b/tests/tutorial/test_datastore.py @@ -29,4 +29,5 @@ def test_sample_interactions(registry): def test_main(): from tutorial.datastore import main + assert None is main() diff --git a/tests/tutorial/test_overrides.py b/tests/tutorial/test_overrides.py index e77c438..2563b7a 100644 --- a/tests/tutorial/test_overrides.py +++ b/tests/tutorial/test_overrides.py @@ -29,4 +29,5 @@ def test_sample_interactions(registry): def test_main(): from tutorial.overrides import main + assert None is main() From cde7ab5630b4683aab79814311cf3c0d272bc544 Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Fri, 27 Nov 2020 19:22:56 -0500 Subject: [PATCH 06/13] Keep examples out of the sdist. --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index eb21927..973e7b1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,3 +13,4 @@ include .coveragerc .flake8 include tox.ini readthedocs.yml recursive-exclude * __pycache__ *.py[cod] +recursive-include examples *.py From d562a58ddf2011e8cb65b95ec86c9af865f9aca4 Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Fri, 27 Nov 2020 20:18:54 -0500 Subject: [PATCH 07/13] Move the __wired_factory__ sniffing from the decorator to container.py and write tests there. --- examples/decorators/register_factory_basic.py | 26 +++++++++++++++++++ examples/decorators/register_wired_factory.py | 26 +++++++++++++++++++ src/wired/container.py | 4 ++- src/wired/decorators.py | 7 ++--- tests/decorators/conftest.py | 5 ++-- .../decorators/test_register_factory_basic.py | 19 ++++++++++++++ .../test_register_factory_wired_factory.py | 17 ++++++++++++ tests/test_container.py | 26 +++++++++++++++++-- 8 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 examples/decorators/register_factory_basic.py create mode 100644 examples/decorators/register_wired_factory.py create mode 100644 tests/decorators/test_register_factory_basic.py create mode 100644 tests/decorators/test_register_factory_wired_factory.py diff --git a/examples/decorators/register_factory_basic.py b/examples/decorators/register_factory_basic.py new file mode 100644 index 0000000..87d3db6 --- /dev/null +++ b/examples/decorators/register_factory_basic.py @@ -0,0 +1,26 @@ +""" +:func:`wired.ServiceRegistry.register_factory` can sniff for a +``__wired_factory__`` class method to use as a class's factory. +""" +from wired import ServiceContainer + + +class Greeter: + def __init__(self, name): + self.name = name + + +class Greeting: + def __init__(self, container: ServiceContainer): + self.greeter = container.get(Greeter) + + def greet(self): + return f'Hello from {self.greeter.name}' + + +def greeter_factory(container): + return Greeter('Marie') + + +def greeting_factory(container): + return Greeting(container) diff --git a/examples/decorators/register_wired_factory.py b/examples/decorators/register_wired_factory.py new file mode 100644 index 0000000..9afc19f --- /dev/null +++ b/examples/decorators/register_wired_factory.py @@ -0,0 +1,26 @@ +""" +A class is a callable that can be the factory supplied to +:func:`wired.ServiceRegistry.register_factory`. +""" +from wired import ServiceContainer + + +class Greeter: + def __init__(self, name): + self.name = name + + @classmethod + def __wired_factory__(cls, container: ServiceContainer): + return cls('Marie') + + +class Greeting: + def __init__(self, container: ServiceContainer): + self.greeter = container.get(Greeter) + + def greet(self): + return f'Hello from {self.greeter.name}' + + @classmethod + def __wired_factory__(cls, container: ServiceContainer): + return cls(container) diff --git a/src/wired/container.py b/src/wired/container.py index 02f0230..4a056d9 100644 --- a/src/wired/container.py +++ b/src/wired/container.py @@ -33,7 +33,9 @@ class IContextFinalizer(Interface): class ServiceFactoryInfo: def __init__(self, factory, service_iface, context_iface, wants_context): - self.factory = factory + # Use the __wired_factory__ protocol if present + _factory = getattr(factory, '__wired_factory__', factory) + self.factory = _factory self.service_iface = service_iface self.context_iface = context_iface self.wants_context = wants_context diff --git a/src/wired/decorators.py b/src/wired/decorators.py index 9451fb6..18d16a0 100644 --- a/src/wired/decorators.py +++ b/src/wired/decorators.py @@ -1,7 +1,7 @@ # wired is usable without venusian try: import venusian -except ImportError: +except ImportError: # pragma: no cover venusian = None @@ -51,10 +51,7 @@ def callback(scanner: venusian.Scanner, name: str, cls): # class as the instance for_ = self.for_ if self.for_ else cls - def _default_factory(container): - return cls(container) - - _factory = getattr(cls, '__wired_factory__', _default_factory) + _factory = getattr(cls, '__wired_factory__', cls) registry.register_factory( _factory, for_, context=self.context, name=self.name ) diff --git a/tests/decorators/conftest.py b/tests/decorators/conftest.py index dafceeb..4903aa5 100644 --- a/tests/decorators/conftest.py +++ b/tests/decorators/conftest.py @@ -13,7 +13,7 @@ @pytest.fixture def scannable(): """ Each test file needs to implement this """ - raise NotImplementedError() + return None @pytest.fixture @@ -24,6 +24,7 @@ def registry(): @pytest.fixture def container(registry, scannable): s = Scanner(registry=registry) - s.scan(scannable) + if scannable is not None: + s.scan(scannable) c = registry.create_container() return c diff --git a/tests/decorators/test_register_factory_basic.py b/tests/decorators/test_register_factory_basic.py new file mode 100644 index 0000000..a57ce79 --- /dev/null +++ b/tests/decorators/test_register_factory_basic.py @@ -0,0 +1,19 @@ +""" +Use :func:`wired.ServiceRegistry.register_factory` with +a class as the factory callable, rather than a separate +function. +""" + + +def test_greeter(container, registry): + from examples.decorators.register_factory_basic import ( + greeter_factory, + Greeter, + greeting_factory, + Greeting, + ) + + registry.register_factory(greeter_factory, Greeter) + registry.register_factory(greeting_factory, Greeting) + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/test_register_factory_wired_factory.py b/tests/decorators/test_register_factory_wired_factory.py new file mode 100644 index 0000000..84a7a92 --- /dev/null +++ b/tests/decorators/test_register_factory_wired_factory.py @@ -0,0 +1,17 @@ +""" +Use :func:`wired.ServiceRegistry.register_factory` with +a class that has the ``__wired_factory__`` class method +but no decorator.. +""" + + +def test_greeter(container, registry): + from examples.decorators.register_wired_factory import ( + Greeter, + Greeting, + ) + + registry.register_factory(Greeter, Greeter) + registry.register_factory(Greeting, Greeting) + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/tests/test_container.py b/tests/test_container.py index 73cb7f1..438e504 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -1,6 +1,5 @@ import pytest -from zope.interface import Interface -from zope.interface import implementer +from zope.interface import Interface, implementer class IFooService(Interface): @@ -48,6 +47,20 @@ def __call__(self, container): return self.result +class DummyWiredFactory: + def __init__(self, result=None): + if result is None: + result = DummyService() + self.result = result + self.calls = [] + + @classmethod + def __wired_factory__(cls, container): + inst = cls() + inst.calls.append(container) + return inst + + @pytest.fixture def registry(): from wired import ServiceRegistry @@ -83,6 +96,15 @@ def test_various_params(registry, iface, contexts, name): assert len(factory.calls) == 1 +def test_wired_factory(registry): + registry.register_factory(DummyWiredFactory, DummyWiredFactory) + c1 = registry.create_container() + result = c1.get(DummyWiredFactory) + assert isinstance(result, DummyWiredFactory) + assert result.calls[0] is c1 + assert len(result.calls) == 1 + + def test_basic_caching(registry): factory = DummyFactory() registry.register_factory(factory, name='foo') From 07e7e9467f5ed6c7e5797bf051bc34a4fcc0d47c Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Sun, 29 Nov 2020 10:44:30 -0500 Subject: [PATCH 08/13] Rename the factory decorator to service_factory. --- examples/decorators/basic_class.py | 6 ++--- .../decorators/wired_factory_classmethod.py | 6 ++--- src/wired/__init__.py | 4 ++-- src/wired/decorators.py | 22 +------------------ 4 files changed, 9 insertions(+), 29 deletions(-) diff --git a/examples/decorators/basic_class.py b/examples/decorators/basic_class.py index 6076fb6..daae1da 100644 --- a/examples/decorators/basic_class.py +++ b/examples/decorators/basic_class.py @@ -1,8 +1,8 @@ """ -Simplest example for ``@factory`` decorator: a basic class. +Simplest example for ``@service_factory`` decorator: a basic class. """ from wired import ServiceContainer -from wired import factory +from wired import service_factory class Greeter: @@ -10,7 +10,7 @@ def __init__(self, name): self.name = name -@factory() +@service_factory() class Greeting: def __init__(self, container: ServiceContainer): self.greeter = container.get(Greeter) diff --git a/examples/decorators/wired_factory_classmethod.py b/examples/decorators/wired_factory_classmethod.py index f3d3b7a..5700bc2 100644 --- a/examples/decorators/wired_factory_classmethod.py +++ b/examples/decorators/wired_factory_classmethod.py @@ -2,10 +2,10 @@ Simple usage of a ``_wired_factory__`` classmethod. """ from wired import ServiceContainer -from wired import factory +from wired import service_factory -@factory() +@service_factory() class Greeter: def __init__(self, name): self.name = name @@ -15,7 +15,7 @@ def __wired_factory__(cls, container: ServiceContainer): return Greeter('Marie') -@factory() +@service_factory() class Greeting: def __init__(self, container: ServiceContainer): self.greeter = container.get(Greeter) diff --git a/src/wired/__init__.py b/src/wired/__init__.py index a650a36..579387e 100644 --- a/src/wired/__init__.py +++ b/src/wired/__init__.py @@ -1,5 +1,5 @@ -__all__ = ['ServiceContainer', 'ServiceRegistry', 'factory'] +__all__ = ['ServiceContainer', 'ServiceRegistry', 'service_factory'] from .container import ServiceContainer from .container import ServiceRegistry -from .decorators import factory +from .decorators import service_factory diff --git a/src/wired/decorators.py b/src/wired/decorators.py index 18d16a0..9ad196a 100644 --- a/src/wired/decorators.py +++ b/src/wired/decorators.py @@ -6,33 +6,13 @@ # noinspection PyPep8Naming -class factory: +class service_factory: """ Register a factory for a class that can sniff dependencies. The factory will be registered with a :class:`wired.ServiceRegistry` when performing a venusian scan. - .. code-block:: python - - from sqlalchemy.orm import Session - - @factory - @dataclass - class LoginService: - db: Session - - # ... later - - registry = ServiceRegistry() - scanner = venusian.Scanner(registry=registry) - scanner.scan() - - # ... later - - container = registry.create_container() - svc = container.get(LoginService) - .. seealso:: - :func:`wired.ServiceRegistry.register_factory` From 0db7a181426629449b77f51df6fb19eb81083faf Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Sun, 29 Nov 2020 11:39:02 -0500 Subject: [PATCH 09/13] Begin adding the usage changes and start the examples section in the docs. --- docs/api.rst | 3 ++ docs/examples.rst | 14 ++++++++++ docs/index.rst | 1 + docs/usage.rst | 44 ++++++++++++++++++++++++++++++ examples/decorators/basic_class.py | 13 ++++----- 5 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 docs/examples.rst diff --git a/docs/api.rst b/docs/api.rst index 6303651..81e74c4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,6 +10,9 @@ .. autoclass:: ServiceRegistry :members: +.. autoclass:: service_factory + :members: + :mod:`wired.dataclasses` API ============================ diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..0347314 --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,14 @@ +======== +Examples +======== + +.. _examples-decorators: + +Decorators +~~~~~~~~~~ + +.. _examples-wired-factory: + +Wired Factory +~~~~~~~~~~~~~ + diff --git a/docs/index.rst b/docs/index.rst index 6672ccf..7d7d5f1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -108,6 +108,7 @@ More Information :maxdepth: 1 usage + examples tutorial/index dc/index dc/usage diff --git a/docs/usage.rst b/docs/usage.rst index 838c2bf..361dccb 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -41,10 +41,53 @@ However, a service factory that is registered to provide instances of the ``Logi Anyone else registering such a service factory is directly competing for control of that type. It is possible to register for both type and name. +As a note, when calling :meth:`wired.ServiceRegistry.register_factory`, the first argument is a callable. +It doesn't have to be a function: it could be a class that accepts the container as an argument. +In such a case, the class might be *both* arguments. +This kind of usage is helpful when combined with the ``__wired_factory__`` protocol and the ``@service_factory`` decorator discussed below. + Service factories accept one argument, a :class:`wired.ServiceContainer` instance. The container may be used to get any dependencies required to create the service and return it from the factory. The service is then cached on the container, available for any other factories or code to get. +The ``@service_factory`` decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's quite convenient to register your factories with decorators, as you can just point at a module or package and not manually do each registration. +``wired`` has optional support for the `venusian `_ package for deferred scanning and evaluation of decorators. +You can then register your services with the :class:`wired.service_factory` decorator: + +.. literalinclude:: ../examples/decorators/basic_class.py + :start-at: import service_factory + :end-at: Hello from + +The decorator can take arguments of ``for_``, ``context``, and ``name``, to mimic the arguments to ``register_factory``. + +You can find more variations, including setup of the scanner, on this in the :ref:`examples-decorators` examples. + +The ``__wired_factory__`` protocol +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you call :meth:`wired.ServiceRegistry.register_factory` the first argument is the factory "function". +It takes a :class:`wired.ServiceContainer`, makes the desired object, and returns it. +This means a separation: a "function" that makes the class instance, and the class. + +As we saw above, the first argument is really a callable that returns an instance, and that means the class itself can be the first argument. +But what if you want custom logic to pick apart the container then construct the class? + +Enter the ``__wired_factory__`` protocol. +This is an attribute -- for example, a ``classmethod`` -- on the factory callable. +It is passed the container and returns the class. + +.. literalinclude:: ../examples/decorators/wired_factory_classmethod.py + :start-at: @service_factory() + :end-at: return Greeter + +This class method is then used as the first argument to :meth:`wired.ServiceRegistry.register_factory`. +It doesn't have to be just for classes and class methods: a function/class/instance could have a ``__wired_factory__`` attribute stamped on it, possibly via an intermediate decorator. + +More examples are available in :ref:`examples-wired-factory`. + Example ~~~~~~~ @@ -130,3 +173,4 @@ For example, imagine binding the web request itself as a service, or the active # later ... user = container.get(IUser) + diff --git a/examples/decorators/basic_class.py b/examples/decorators/basic_class.py index daae1da..e7fb687 100644 --- a/examples/decorators/basic_class.py +++ b/examples/decorators/basic_class.py @@ -1,23 +1,22 @@ """ Simplest example for ``@service_factory`` decorator: a basic class. """ -from wired import ServiceContainer from wired import service_factory -class Greeter: - def __init__(self, name): - self.name = name - - @service_factory() class Greeting: - def __init__(self, container: ServiceContainer): + def __init__(self, container): self.greeter = container.get(Greeter) def greet(self): return f'Hello from {self.greeter.name}' +class Greeter: + def __init__(self, name): + self.name = name + + def greeter_factory(container): return Greeter('Marie') From a8e563986ec5391094b345b859e9d15041d9d881 Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Sun, 29 Nov 2020 12:43:22 -0500 Subject: [PATCH 10/13] Convert the decorator examples to new-style included in docs. --- docs/examples.rst | 50 +++++++++++++++++++ docs/usage.rst | 2 +- examples/__init__.py | 1 + examples/decorators/__init__.py | 1 + examples/decorators/basic_class.py | 22 +++++++- .../decorator_with_wired_factory.py | 46 +++++++++++++++++ examples/decorators/no_decorator.py | 27 +++++++--- examples/decorators/no_decorator_class.py | 14 +++++- examples/decorators/register_factory_basic.py | 26 ---------- .../register_wired_factory.py | 0 .../wired_factory_classmethod.py | 0 tests/decorators/test_no_decorator.py | 41 +++++++-------- 12 files changed, 170 insertions(+), 60 deletions(-) create mode 100644 examples/__init__.py create mode 100644 examples/decorators/__init__.py create mode 100644 examples/decorators/decorator_with_wired_factory.py delete mode 100644 examples/decorators/register_factory_basic.py rename examples/{decorators => wired_factory}/register_wired_factory.py (100%) rename examples/{decorators => wired_factory}/wired_factory_classmethod.py (100%) diff --git a/docs/examples.rst b/docs/examples.rst index 0347314..91fb09d 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -7,6 +7,56 @@ Examples Decorators ~~~~~~~~~~ +Let's show the use of `venusian `_ and the :class:`wired.service_factory` decorator in building an app that scans for factories. +We'll do it piece-by-piece, starting with a regular ``wired`` app. + +Basic ``wired`` app +------------------- + +As a starting point we use an app with *no* decorators. +In this app we have a ``Greeting`` class that depends on a ``Greeter`` class. +As such, we register a factory for each. + +.. literalinclude:: ../examples/decorators/no_decorator.py + +This is the basics of a simple, pluggable application. +As a note, everything in the ``app`` function would typically be done once as part of your app. + +Class as factory +---------------- + +Before getting to decorators, just to emphasize...the first argument to :meth:`wired.ServiceRegistry.register_factory` can be the class itself. + +.. literalinclude:: ../examples/decorators/no_decorator_class.py + :emphasize-lines: 25 + +``venusian`` scanner +-------------------- + +We will now add ``venusian`` and its ``Scanner``. +We make a ``Scanner`` instance and include the ``registry``. +When we call ``scan`` on a module -- in this case, the same module -- it looks for the ``@service_factory`` decorator. +The decorator then extracts the ``registry`` instance we stored in the ``Scanner`` and does the registration. + +.. literalinclude:: ../examples/decorators/basic_class.py + +What's nice about this venusian approach: no module-level state globals stuff. + +Another decorator plus ``__wired_factory__`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We'll now move the ``Greeter`` class to also use the ``@service_factory`` decorator instead of a manual registration. +Since it hard-codes ``Marie`` as a value to the constructor, we use the ``__wired_factory__`` protocol as a class method to generate the instance. +This means any code that does ``container.get(Greeter)`` will run this class method to construct the ``Greeter``. + +.. literalinclude:: ../examples/decorators/decorator_with_wired_factory.py + +We also add a ``__wired_factory__`` class method to ``Greeting`` to make it nicer. +Now its constructor no longer uses the ``container``, which is a huge surface area. +Instead, the class is constructed just with the data it needs, which is nice for testing. +The class method acts as an "adapter", getting stuff out of the container that is needed for the class. + + .. _examples-wired-factory: Wired Factory diff --git a/docs/usage.rst b/docs/usage.rst index 361dccb..c9a4432 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -79,7 +79,7 @@ Enter the ``__wired_factory__`` protocol. This is an attribute -- for example, a ``classmethod`` -- on the factory callable. It is passed the container and returns the class. -.. literalinclude:: ../examples/decorators/wired_factory_classmethod.py +.. literalinclude:: ../examples/wired_factory/wired_factory_classmethod.py :start-at: @service_factory() :end-at: return Greeter diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1 @@ +# diff --git a/examples/decorators/__init__.py b/examples/decorators/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/examples/decorators/__init__.py @@ -0,0 +1 @@ +# diff --git a/examples/decorators/basic_class.py b/examples/decorators/basic_class.py index e7fb687..3a7c621 100644 --- a/examples/decorators/basic_class.py +++ b/examples/decorators/basic_class.py @@ -1,7 +1,10 @@ """ Simplest example for ``@service_factory`` decorator: a basic class. """ -from wired import service_factory +from venusian import Scanner + +from wired import service_factory, ServiceRegistry +from .. import decorators @service_factory() @@ -20,3 +23,20 @@ def __init__(self, name): def greeter_factory(container): return Greeter('Marie') + + +def app(): + # Do this once at startup + registry = ServiceRegistry() + scanner = Scanner(registry=registry) + # Point the scanner at a package/module and scan + scanner.scan(decorators.basic_class) + + registry.register_factory(greeter_factory, Greeter) + # No longer need this line + # registry.register_factory(Greeting, Greeting) + + # Do this for every "request" or operation + container = registry.create_container() + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/examples/decorators/decorator_with_wired_factory.py b/examples/decorators/decorator_with_wired_factory.py new file mode 100644 index 0000000..0729ea4 --- /dev/null +++ b/examples/decorators/decorator_with_wired_factory.py @@ -0,0 +1,46 @@ +""" +Decorators for both plus usage of the ``__wired_factory__ protocol. +""" +from venusian import Scanner + +from wired import service_factory, ServiceRegistry +from .. import decorators + + +@service_factory() +class Greeter: + def __init__(self, name): + self.name = name + + @classmethod + def __wired_factory__(cls, container): + return cls('Marie') + + +@service_factory() +class Greeting: + greeter: Greeter + + def __init__(self, greeter: Greeter): + self.greeter = greeter + + def greet(self): + return f'Hello from {self.greeter.name}' + + @classmethod + def __wired_factory__(cls, container): + greeter = container.get(Greeter) + return cls(greeter) + + +def app(): + # Do this once at startup + registry = ServiceRegistry() + scanner = Scanner(registry=registry) + # Point the scanner at a package/module and scan + scanner.scan(decorators.decorator_with_wired_factory) + + # Do this for every "request" or operation + container = registry.create_container() + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/examples/decorators/no_decorator.py b/examples/decorators/no_decorator.py index 0be411c..1049fe6 100644 --- a/examples/decorators/no_decorator.py +++ b/examples/decorators/no_decorator.py @@ -1,4 +1,4 @@ -from wired import ServiceContainer +from wired import ServiceRegistry, ServiceContainer class Greeter: @@ -6,17 +6,30 @@ def __init__(self, name): self.name = name +def greeter_factory(container): + return Greeter('Marie') + + class Greeting: - def __init__(self, container: ServiceContainer): - self.greeter = container.get(Greeter) + def __init__(self, greeter: Greeter): + self.greeter = greeter def greet(self): return f'Hello from {self.greeter.name}' -def greeter_factory(container): - return Greeter('Marie') +def greeting_factory(container: ServiceContainer): + greeter = container.get(Greeter) + return Greeting(greeter) + +def app(): + # Do this once at startup + registry = ServiceRegistry() + registry.register_factory(greeter_factory, Greeter) + registry.register_factory(greeting_factory, Greeting) -def greeting_factory(container): - return Greeting(container) + # Do this for every "request" or operation + container = registry.create_container() + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/examples/decorators/no_decorator_class.py b/examples/decorators/no_decorator_class.py index e069ab5..edc13a2 100644 --- a/examples/decorators/no_decorator_class.py +++ b/examples/decorators/no_decorator_class.py @@ -1,4 +1,4 @@ -from wired import ServiceContainer +from wired import ServiceContainer, ServiceRegistry class Greeter: @@ -16,3 +16,15 @@ def greet(self): def greeter_factory(container): return Greeter('Marie') + + +def app(): + # Do this once at startup + registry = ServiceRegistry() + registry.register_factory(greeter_factory, Greeter) + registry.register_factory(Greeting, Greeting) + + # Do this for every "request" or operation + container = registry.create_container() + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/examples/decorators/register_factory_basic.py b/examples/decorators/register_factory_basic.py deleted file mode 100644 index 87d3db6..0000000 --- a/examples/decorators/register_factory_basic.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -:func:`wired.ServiceRegistry.register_factory` can sniff for a -``__wired_factory__`` class method to use as a class's factory. -""" -from wired import ServiceContainer - - -class Greeter: - def __init__(self, name): - self.name = name - - -class Greeting: - def __init__(self, container: ServiceContainer): - self.greeter = container.get(Greeter) - - def greet(self): - return f'Hello from {self.greeter.name}' - - -def greeter_factory(container): - return Greeter('Marie') - - -def greeting_factory(container): - return Greeting(container) diff --git a/examples/decorators/register_wired_factory.py b/examples/wired_factory/register_wired_factory.py similarity index 100% rename from examples/decorators/register_wired_factory.py rename to examples/wired_factory/register_wired_factory.py diff --git a/examples/decorators/wired_factory_classmethod.py b/examples/wired_factory/wired_factory_classmethod.py similarity index 100% rename from examples/decorators/wired_factory_classmethod.py rename to examples/wired_factory/wired_factory_classmethod.py diff --git a/tests/decorators/test_no_decorator.py b/tests/decorators/test_no_decorator.py index 3fb2206..23a4a7e 100644 --- a/tests/decorators/test_no_decorator.py +++ b/tests/decorators/test_no_decorator.py @@ -1,28 +1,21 @@ -""" -Starting-point example that uses no decorator nor -``wired_factory`` support but the ``Greeting`` class - serves as its factory. - -""" import pytest -from examples.decorators import no_decorator - - -@pytest.fixture -def scannable(): - return no_decorator - +from examples.decorators import ( + no_decorator, + no_decorator_class, + basic_class, + decorator_with_wired_factory, +) -def test_greeter(container, registry): - from examples.decorators.no_decorator import ( - greeter_factory, - Greeter, - greeting_factory, - Greeting, - ) - registry.register_factory(greeter_factory, Greeter) - registry.register_factory(greeting_factory, Greeting) - greeting: Greeting = container.get(Greeting) - assert 'Hello from Marie' == greeting.greet() +@pytest.mark.parametrize( + 'target', + ( + no_decorator, + no_decorator_class, + basic_class, + decorator_with_wired_factory, + ), +) +def test_greeter(target): + target.app() From 8704d21d070f485d62ad35d4f41679326a3c4938 Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Sun, 29 Nov 2020 13:42:18 -0500 Subject: [PATCH 11/13] Finish converting the examples to the new-style and integrating them into narrative docs. --- docs/examples.rst | 35 ++++++++- docs/usage.rst | 6 +- examples/decorators/decorator_args.py | 77 +++++++++++++++++++ .../wired_factory/register_wired_factory.py | 24 ++++-- .../wired_factory_classmethod.py | 24 ------ tests/decorators/__init__.py | 0 tests/decorators/test_basic_class.py | 20 ----- tests/decorators/test_no_decorator_class.py | 26 ------- .../decorators/test_register_factory_basic.py | 19 ----- .../test_register_factory_wired_factory.py | 17 ---- .../test_wired_factory_classmethod.py | 15 ---- tests/{decorators => examples}/conftest.py | 0 .../test_examples.py} | 12 ++- 13 files changed, 141 insertions(+), 134 deletions(-) create mode 100644 examples/decorators/decorator_args.py delete mode 100644 examples/wired_factory/wired_factory_classmethod.py delete mode 100644 tests/decorators/__init__.py delete mode 100644 tests/decorators/test_basic_class.py delete mode 100644 tests/decorators/test_no_decorator_class.py delete mode 100644 tests/decorators/test_register_factory_basic.py delete mode 100644 tests/decorators/test_register_factory_wired_factory.py delete mode 100644 tests/decorators/test_wired_factory_classmethod.py rename tests/{decorators => examples}/conftest.py (100%) rename tests/{decorators/test_no_decorator.py => examples/test_examples.py} (50%) diff --git a/docs/examples.rst b/docs/examples.rst index 91fb09d..760ff96 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -43,7 +43,7 @@ The decorator then extracts the ``registry`` instance we stored in the ``Scanner What's nice about this venusian approach: no module-level state globals stuff. Another decorator plus ``__wired_factory__`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------------------------- We'll now move the ``Greeter`` class to also use the ``@service_factory`` decorator instead of a manual registration. Since it hard-codes ``Marie`` as a value to the constructor, we use the ``__wired_factory__`` protocol as a class method to generate the instance. @@ -56,9 +56,42 @@ Now its constructor no longer uses the ``container``, which is a huge surface ar Instead, the class is constructed just with the data it needs, which is nice for testing. The class method acts as an "adapter", getting stuff out of the container that is needed for the class. +Decorator arguments +------------------- + +The ``@service_factory`` acts as a replacement for ``register_factory``. +Thus it needs to support the other arguments beyond the first one: + +- The ``service_or_iface``, if not provided, defaults to the class the decorator is decorating +- If you pass ``for_=`` to the decorator, it will be used as the ``service_or_iface`` argument to +- You can also pass ``context=`` and ``name=`` + +Imagine our app now has a ``Customer`` and ``FrenchCustomer`` as container contexts. +Here is an example of registering different ``Greeter`` classes that are unique to those contexts: + +.. literalinclude:: ../examples/decorators/decorator_args.py .. _examples-wired-factory: Wired Factory ~~~~~~~~~~~~~ +Registering a factory means two things: a callable that constructs and returns an object, then the "kind" of thing the factory is registered for. +You can eliminate the callable as a separate function by providing a ``__wired_factory__`` callable *on*, for example, the class that gets constructed. + +This is the wired factory "protocol" and the callable acts as an adapter. +It is handed the container, extracts what it needs, then constructs and returns an object. + +Basic wired factory callable +---------------------------- + +We start again with our simple app, with a ``Greeting`` that uses a ``Greeter``. +In this case, we do two things: + +- Both classes have a ``classmethod`` that manages construction of instances +- The ``register_factory`` first argument is, thus, the class itself + +.. literalinclude:: ../examples/wired_factory/register_wired_factory.py + +With this, when some application code calls ``container.get(Greeter)``, the construction is done by ``Greeter.__wired_factory__``. + diff --git a/docs/usage.rst b/docs/usage.rst index c9a4432..c46b548 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -79,9 +79,9 @@ Enter the ``__wired_factory__`` protocol. This is an attribute -- for example, a ``classmethod`` -- on the factory callable. It is passed the container and returns the class. -.. literalinclude:: ../examples/wired_factory/wired_factory_classmethod.py - :start-at: @service_factory() - :end-at: return Greeter +.. literalinclude:: ../examples/wired_factory/register_wired_factory.py + :start-at: @classmethod + :end-at: return cls This class method is then used as the first argument to :meth:`wired.ServiceRegistry.register_factory`. It doesn't have to be just for classes and class methods: a function/class/instance could have a ``__wired_factory__`` attribute stamped on it, possibly via an intermediate decorator. diff --git a/examples/decorators/decorator_args.py b/examples/decorators/decorator_args.py new file mode 100644 index 0000000..abe2d29 --- /dev/null +++ b/examples/decorators/decorator_args.py @@ -0,0 +1,77 @@ +""" +Decorators for both plus usage of the ``__wired_factory__ protocol. +""" +from venusian import Scanner + +from wired import service_factory, ServiceRegistry +from .. import decorators + + +class Customer: + def __init__(self): + self.name = 'Jill' + + +class FrenchCustomer(Customer): + def __init__(self): + super().__init__() + self.name = 'Juliette' + + +@service_factory(context=Customer) +class Greeter: + def __init__(self, name): + self.name = name + + @classmethod + def __wired_factory__(cls, container): + return cls('Susan') + + +@service_factory(for_=Greeter, context=FrenchCustomer) +class FrenchGreeter: + """ Serves as Greeter when container.context is FrenchCustomer """ + def __init__(self, name): + self.name = name + + @classmethod + def __wired_factory__(cls, container): + return cls('Marie') + + +@service_factory(context=Customer) +class Greeting: + greeter: Greeter + + def __init__(self, greeter: Greeter, customer): + self.greeter = greeter + self.customer = customer + + def greet(self): + return f'Hello from {self.greeter.name} to {self.customer.name}' + + @classmethod + def __wired_factory__(cls, container): + greeter = container.get(Greeter) + customer = container.context + return cls(greeter, customer) + + +def app(): + # Do this once at startup + registry = ServiceRegistry() + scanner = Scanner(registry=registry) + # Point the scanner at a package/module and scan + scanner.scan(decorators.decorator_args) + + # First request, for a regular Customer + customer1 = Customer() + container1 = registry.create_container(context=customer1) + greeting1: Greeting = container1.get(Greeting) + assert 'Hello from Susan to Jill' == greeting1.greet() + + # Second request, for a FrenchCustomer + customer2 = FrenchCustomer() + container2 = registry.create_container(context=customer2) + greeting2: Greeting = container2.get(Greeting) + assert 'Hello from Marie to Juliette' == greeting2.greet() diff --git a/examples/wired_factory/register_wired_factory.py b/examples/wired_factory/register_wired_factory.py index 9afc19f..7020df0 100644 --- a/examples/wired_factory/register_wired_factory.py +++ b/examples/wired_factory/register_wired_factory.py @@ -2,7 +2,8 @@ A class is a callable that can be the factory supplied to :func:`wired.ServiceRegistry.register_factory`. """ -from wired import ServiceContainer + +from wired import ServiceContainer, ServiceRegistry class Greeter: @@ -15,12 +16,25 @@ def __wired_factory__(cls, container: ServiceContainer): class Greeting: - def __init__(self, container: ServiceContainer): - self.greeter = container.get(Greeter) + def __init__(self, greeter: Greeter): + self.greeter = greeter def greet(self): return f'Hello from {self.greeter.name}' @classmethod - def __wired_factory__(cls, container: ServiceContainer): - return cls(container) + def __wired_factory__(cls, container): + greeter = container.get(Greeter) + return cls(greeter) + + +def app(): + # Do this once at startup + registry = ServiceRegistry() + registry.register_factory(Greeter, Greeter) + registry.register_factory(Greeting, Greeting) + + # Do this for every "request" or operation + container = registry.create_container() + greeting: Greeting = container.get(Greeting) + assert 'Hello from Marie' == greeting.greet() diff --git a/examples/wired_factory/wired_factory_classmethod.py b/examples/wired_factory/wired_factory_classmethod.py deleted file mode 100644 index 5700bc2..0000000 --- a/examples/wired_factory/wired_factory_classmethod.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Simple usage of a ``_wired_factory__`` classmethod. -""" -from wired import ServiceContainer -from wired import service_factory - - -@service_factory() -class Greeter: - def __init__(self, name): - self.name = name - - @classmethod - def __wired_factory__(cls, container: ServiceContainer): - return Greeter('Marie') - - -@service_factory() -class Greeting: - def __init__(self, container: ServiceContainer): - self.greeter = container.get(Greeter) - - def greet(self): - return f'Hello from {self.greeter.name}' diff --git a/tests/decorators/__init__.py b/tests/decorators/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/decorators/test_basic_class.py b/tests/decorators/test_basic_class.py deleted file mode 100644 index 93f1249..0000000 --- a/tests/decorators/test_basic_class.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest - -from examples.decorators import basic_class - - -@pytest.fixture -def scannable(): - return basic_class - - -def test_greeter(container, registry): - from examples.decorators.basic_class import ( - greeter_factory, - Greeter, - Greeting, - ) - - registry.register_factory(greeter_factory, Greeter) - greeting: Greeting = container.get(Greeting) - assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/test_no_decorator_class.py b/tests/decorators/test_no_decorator_class.py deleted file mode 100644 index 3facde7..0000000 --- a/tests/decorators/test_no_decorator_class.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Starting-point example that uses no decorator nor -``wired_factory`` support. - -""" -import pytest - -from examples.decorators import no_decorator_class - - -@pytest.fixture -def scannable(): - return no_decorator_class - - -def test_greeter(container, registry): - from examples.decorators.no_decorator_class import ( - greeter_factory, - Greeter, - Greeting, - ) - - registry.register_factory(greeter_factory, Greeter) - registry.register_factory(Greeting, Greeting) - greeting: Greeting = container.get(Greeting) - assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/test_register_factory_basic.py b/tests/decorators/test_register_factory_basic.py deleted file mode 100644 index a57ce79..0000000 --- a/tests/decorators/test_register_factory_basic.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Use :func:`wired.ServiceRegistry.register_factory` with -a class as the factory callable, rather than a separate -function. -""" - - -def test_greeter(container, registry): - from examples.decorators.register_factory_basic import ( - greeter_factory, - Greeter, - greeting_factory, - Greeting, - ) - - registry.register_factory(greeter_factory, Greeter) - registry.register_factory(greeting_factory, Greeting) - greeting: Greeting = container.get(Greeting) - assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/test_register_factory_wired_factory.py b/tests/decorators/test_register_factory_wired_factory.py deleted file mode 100644 index 84a7a92..0000000 --- a/tests/decorators/test_register_factory_wired_factory.py +++ /dev/null @@ -1,17 +0,0 @@ -""" -Use :func:`wired.ServiceRegistry.register_factory` with -a class that has the ``__wired_factory__`` class method -but no decorator.. -""" - - -def test_greeter(container, registry): - from examples.decorators.register_wired_factory import ( - Greeter, - Greeting, - ) - - registry.register_factory(Greeter, Greeter) - registry.register_factory(Greeting, Greeting) - greeting: Greeting = container.get(Greeting) - assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/test_wired_factory_classmethod.py b/tests/decorators/test_wired_factory_classmethod.py deleted file mode 100644 index 496ab82..0000000 --- a/tests/decorators/test_wired_factory_classmethod.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from examples.decorators import wired_factory_classmethod - - -@pytest.fixture -def scannable(): - return wired_factory_classmethod - - -def test_greeter(container, registry): - from examples.decorators.wired_factory_classmethod import Greeting - - greeting: Greeting = container.get(Greeting) - assert 'Hello from Marie' == greeting.greet() diff --git a/tests/decorators/conftest.py b/tests/examples/conftest.py similarity index 100% rename from tests/decorators/conftest.py rename to tests/examples/conftest.py diff --git a/tests/decorators/test_no_decorator.py b/tests/examples/test_examples.py similarity index 50% rename from tests/decorators/test_no_decorator.py rename to tests/examples/test_examples.py index 23a4a7e..db93ebb 100644 --- a/tests/decorators/test_no_decorator.py +++ b/tests/examples/test_examples.py @@ -5,16 +5,20 @@ no_decorator_class, basic_class, decorator_with_wired_factory, + decorator_args, ) +from examples.wired_factory import register_wired_factory @pytest.mark.parametrize( 'target', ( - no_decorator, - no_decorator_class, - basic_class, - decorator_with_wired_factory, + no_decorator, + no_decorator_class, + basic_class, + decorator_with_wired_factory, + register_wired_factory, + decorator_args, ), ) def test_greeter(target): From fe7b3f5c21983daeda6924f6f84942f8019df982 Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Sun, 29 Nov 2020 15:47:18 -0500 Subject: [PATCH 12/13] Improve coverage (and remove unused fixtures.) --- tests/examples/conftest.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/tests/examples/conftest.py b/tests/examples/conftest.py index 4903aa5..5a9e729 100644 --- a/tests/examples/conftest.py +++ b/tests/examples/conftest.py @@ -1,30 +1,6 @@ import sys -import pytest - -from wired import ServiceRegistry - if sys.version_info < (3, 7): # pragma: no cover collect_ignore_glob = ['*.py'] else: - from venusian import Scanner - - -@pytest.fixture -def scannable(): - """ Each test file needs to implement this """ - return None - - -@pytest.fixture -def registry(): - return ServiceRegistry() - - -@pytest.fixture -def container(registry, scannable): - s = Scanner(registry=registry) - if scannable is not None: - s.scan(scannable) - c = registry.create_container() - return c + pass From 87c34fb1daf8901702b5618d2e06f1566bb68d82 Mon Sep 17 00:00:00 2001 From: Paul Everitt Date: Sun, 29 Nov 2020 16:25:45 -0500 Subject: [PATCH 13/13] Remove fossil, a line that isn't needed. --- src/wired/decorators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wired/decorators.py b/src/wired/decorators.py index 9ad196a..b44d82a 100644 --- a/src/wired/decorators.py +++ b/src/wired/decorators.py @@ -31,9 +31,8 @@ def callback(scanner: venusian.Scanner, name: str, cls): # class as the instance for_ = self.for_ if self.for_ else cls - _factory = getattr(cls, '__wired_factory__', cls) registry.register_factory( - _factory, for_, context=self.context, name=self.name + cls, for_, context=self.context, name=self.name ) venusian.attach(wrapped, callback, category='wired')