diff --git a/docs/conf.py b/docs/conf.py index a78d17b4..dfdc2aa9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,6 +24,7 @@ exclude_patterns = ["_build"] pygments_style = "sphinx" +autodoc_default_options = {"members": True, "show-inheritance": True} highlight_language = "python3" todo_include_todos = False diff --git a/docs/tutorials/echo.rst b/docs/tutorials/echo.rst index c486a08a..9631d7f9 100644 --- a/docs/tutorials/echo.rst +++ b/docs/tutorials/echo.rst @@ -69,11 +69,11 @@ directory:: import anyio - from asphalt.core import Component, Context, run_application + from asphalt.core import Component, run_application class ServerComponent(Component): - async def start(self, ctx: Context) -> None: + async def start(self) -> None: print("Hello, world!") if __name__ == "__main__": @@ -109,10 +109,9 @@ For this purpose, we will use AnyIO's :func:`~anyio.create_tcp_listener` functio from asphalt.core import ( Component, - Context, context_teardown, run_application, - start_service_task, + start_background_task, ) @@ -124,11 +123,11 @@ For this purpose, we will use AnyIO's :func:`~anyio.create_tcp_listener` functio class ServerComponent(Component): @context_teardown - async def start(self, ctx: Context) -> AsyncGenerator[None, Exception | None]: + async def start(self) -> AsyncGenerator[None, Exception | None]: async with await anyio.create_tcp_listener( local_host="localhost", local_port=64100 ) as listener: - start_service_task(lambda: listener.serve(handle), "Echo server") + start_background_task(lambda: listener.serve(handle), "Echo server") yield if __name__ == '__main__': @@ -179,7 +178,7 @@ Create the file ``client.py`` file in the ``echo`` package directory as follows: import anyio - from asphalt.core import CLIApplicationComponent, Context, run_application + from asphalt.core import CLIApplicationComponent, run_application class ClientComponent(CLIApplicationComponent): diff --git a/docs/tutorials/webnotifier.rst b/docs/tutorials/webnotifier.rst index 91dc7343..fd9cc0a9 100644 --- a/docs/tutorials/webnotifier.rst +++ b/docs/tutorials/webnotifier.rst @@ -155,11 +155,11 @@ And to make the the results look nicer in an email message, you can switch to us class ApplicationComponent(CLIApplicationComponent): - async def start(self, ctx: Context) -> None: + async def start(self) -> None: self.add_component( "mailer", backend="smtp", host="your.smtp.server.here", message_defaults={"sender": "your@email.here", "to": "your@email.here"}) - await super().start(ctx) + await super().start() @inject async def run(self, *, mailer: Mailer = resource()) -> None: @@ -272,7 +272,7 @@ Asphalt application:: self.delay = delay @context_teardown - async def start(self, ctx: Context) -> None: + async def start(self) -> None: detector = Detector(self.url, self.delay) await ctx.add_resource(detector) start_service_task(detector.run, "Web page change detector") @@ -298,7 +298,7 @@ become somewhat lighter:: class ApplicationComponent(CLIApplicationComponent): - async def start(self, ctx: Context) -> None: + async def start(self) -> None: self.add_component("detector", ChangeDetectorComponent, url="http://imgur.com") self.add_component( "mailer", backend="smtp", host="your.smtp.server.here", diff --git a/docs/userguide/architecture.rst b/docs/userguide/architecture.rst index f39d2984..723dbd23 100644 --- a/docs/userguide/architecture.rst +++ b/docs/userguide/architecture.rst @@ -1,6 +1,8 @@ Application architecture ======================== +.. py:currentmodule:: asphalt.core + Asphalt applications are centered around the following building blocks: * components @@ -9,11 +11,11 @@ Asphalt applications are centered around the following building blocks: * signals/events * the application runner -*Components* (:class:`~asphalt.core.component.Component`) are classes that initialize one or more +*Components* (:class:`Component`) are classes that initialize one or more services, like network servers or database connections and add them to the *context* as *resources*. Components are started by the application runner and usually discarded afterwards. -*Contexts* (:class:`~asphalt.core.context.Context`) are "hubs" through which *resources* are shared +*Contexts* (:class:`Context`) are "hubs" through which *resources* are shared between components. Contexts can be chained by setting a parent context for a new context. A context has access to all its parents' resources but parent contexts cannot access the resources of their children. @@ -26,12 +28,12 @@ is unique in a context. Events are dispatched asynchronously without blocking the sender. The signal system was loosely modeled after the signal system in the Qt_ toolkit. -The *application runner* (:func:`~asphalt.core.runner.run_application`) is a function that is used +The *application runner* (:func:`~run_application`) is a function that is used to start an Asphalt application. It configures up the Python logging module, sets up an event loop policy (if configured), creates the root context, starts the root component and then runs the event loop until the application exits. A command line tool (``asphalt``) is provided to better facilitate the running of Asphalt applications. It reads the application configuration from one or -more YAML_ formatted configuration files and calls :func:`~asphalt.core.runner.run_application` +more YAML_ formatted configuration files and calls :func:`run_application` with the resulting configuration dictionary as keyword arguments. The settings from the configuration file are merged with hard coded defaults so the config file only needs to override settings where necessary. diff --git a/docs/userguide/components.rst b/docs/userguide/components.rst index a6bdc35e..64dcd0e8 100644 --- a/docs/userguide/components.rst +++ b/docs/userguide/components.rst @@ -1,23 +1,25 @@ Working with components ======================= +.. py:currentmodule:: asphalt.core + Components are the basic building blocks of an Asphalt application. They have a narrowly defined set of responsibilities: #. Take in configuration through the constructor #. Validate the configuration -#. Add resources to the context (in :meth:`~asphalt.core.component.Component.start`) +#. Add resources to the context (in :meth:`Component.start`) #. Close/shut down/clean up resources when the context is torn down (by directly adding a callback - on the context with :meth:`~asphalt.core.context.Context.add_teardown_callback`, or by using - :func:`~asphalt.core.context.context_teardown`) + on the context with :meth:`Context.add_teardown_callback`, or by using + :func:`context_teardown`) -The :meth:`~asphalt.core.component.Component.start` method is called either by the parent component -or the application runner with a :class:`~asphalt.core.context.Context` as its only argument. +The :meth:`Component.start` method is called either by the parent component +or the application runner with a :class:`Context` as its only argument. The component can use the context to add resources for other components and the application business logic to use. It can also request resources provided by other components to provide some complex service that builds on those resources. -The :meth:`~asphalt.core.component.Component.start` method of a component is only called once, +The :meth:`Component.start` method of a component is only called once, during application startup. When all components have been started, they are disposed of. If any of the components raises an exception, the application startup process fails and any context teardown callbacks scheduled so far are called before the process is exited. @@ -49,7 +51,7 @@ The root component of virtually any nontrivial Asphalt application is a containe Container components can of course contain other container components and so on. When the container component starts its child components, each -:meth:`~asphalt.core.component.Component.start` call is launched in its own task. Therefore all the +:meth:`Component.start` call is launched in its own task. Therefore all the child components start concurrently and cannot rely on the start order. This is by design. The only way components should be relying on each other is by the sharing of resources in the context. diff --git a/docs/userguide/contexts.rst b/docs/userguide/contexts.rst index 44b1b628..8cbdb0fc 100644 --- a/docs/userguide/contexts.rst +++ b/docs/userguide/contexts.rst @@ -4,7 +4,7 @@ Working with contexts and resources .. py:currentmodule:: asphalt.core Every Asphalt application has at least one context: the root context. The root context is typically -created by the :func:`~asphalt.core.runner.run_application` function and passed to the root +created by the :func:`run_application` function and passed to the root component. This context will only be closed when the application is shutting down. Most nontrivial applications will make use of *subcontexts*. A subcontext is a context that has a @@ -25,7 +25,7 @@ Contexts are "activated" by entering them using ``async with Context():``, and e that block. When entered, the previous active context becomes the parent context of the new one and the new context becomes the currently active context. When the ``async with`` block is left, the previously active context once again becomes the active context. The currently active context can -be retrieved using :func:`~.context.current_context`. +be retrieved using :func:`current_context`. .. warning:: Activating contexts in asynchronous generators can lead to corruption of the context stack. This is particularly common in asynchronous pytest fixtures because pytest @@ -64,7 +64,7 @@ A static resource can be any arbitrary object (except ``None``). The same object added to the context under several different types, as long as the type/name combination remains unique within the same context. -A resource factory is a callable that takes a :class:`~asphalt.core.context.Context` as +A resource factory is a callable that takes a :class:`Context` as an argument an returns the value of the resource. There are at least a couple reasons to use resource factories instead of static resources: @@ -77,20 +77,20 @@ use resource factories instead of static resources: Getting resources from a context -------------------------------- -The :class:`~asphalt.core.context.Context` class offers a few ways to look up resources. +The :class:`Context` class offers a few ways to look up resources. -The first one, :meth:`~asphalt.core.context.Context.get_resource`, looks for a resource or resource +The first one, :meth:`Context.get_resource`, looks for a resource or resource factory matching the given type and name. If the resource is found, it returns its value. -The second one, :meth:`~asphalt.core.context.Context.require_resource`, works exactly the same way -except that it raises :exc:`~asphalt.core.context.ResourceNotFound` if the resource is not found. +The second one, :meth:`Context.require_resource`, works exactly the same way +except that it raises :exc:`ResourceNotFound` if the resource is not found. -The third method, :meth:`~asphalt.core.context.Context.request_resource`, calls -:meth:`~asphalt.core.context.Context.get_resource` and if the resource is not found, it waits +The third method, :meth:`Context.request_resource`, calls +:meth:`Context.get_resource` and if the resource is not found, it waits indefinitely for the resource to be added to the context or its parents. When that happens, it -calls :meth:`~asphalt.core.context.Context.get_resource` again, at which point success is +calls :meth:`Context.get_resource` again, at which point success is guaranteed. This is usually used only in the components' -:meth:`~asphalt.core.component.Component.start` methods to retrieve resources provided +:meth:`Component.start` methods to retrieve resources provided by sibling components. Resources The order of resource lookup is as follows: @@ -105,8 +105,8 @@ Injecting resources to functions A type-safe way to use context resources is to use `dependency injection`_. In Asphalt, this is done by adding parameters to a function so that they have the resource type as the type annotation, -and a :func:`~.context.resource` instance as the default value. The function then needs to be -decorated using :func:`~.context.inject`:: +and a :func:`resource` instance as the default value. The function then needs to be +decorated using :func:`inject`:: from asphalt.core import inject, resource @@ -115,7 +115,7 @@ decorated using :func:`~.context.inject`:: ... To specify a non-default name for the dependency, you can pass that name as an argument to -:func:`~.context.resource`:: +:func:`resource`:: @inject async def some_function(some_arg, some_resource: MyResourceType = resource('alternate')): @@ -133,7 +133,7 @@ Restrictions: * The resource arguments must not be positional-only arguments * The resources (or their relevant factories) must already be present in the context stack (unless declared optional) when the decorated function is called, or otherwise - :exc:`~.context.ResourceNotFound` is raised + :exc:`ResourceNotFound` is raised .. _dependency injection: https://en.wikipedia.org/wiki/Dependency_injection @@ -143,9 +143,9 @@ Handling resource cleanup Any code that adds resources to a context is also responsible for cleaning them up when the context is closed. This usually involves closing sockets and files and freeing whatever system resources were allocated. This should be done in a *teardown callback*, scheduled using -:meth:`~asphalt.core.context.Context.add_teardown_callback`. When the context is closed, teardown +:meth:`Context.add_teardown_callback`. When the context is closed, teardown callbacks are run in the reverse order in which they were added, and always one at a time, unlike -with the :class:`~asphalt.core.event.Signal` class. This ensures that a resource that is still in +with the :class:`Signal` class. This ensures that a resource that is still in use by another resource is never cleaned up prematurely. For example:: @@ -161,7 +161,7 @@ For example:: ctx.add_resource(service) -There also exists a convenience decorator, :func:`~asphalt.core.context.context_teardown`, which +There also exists a convenience decorator, :func:`context_teardown`, which makes use of asynchronous generators:: from asphalt.core import Component, context_teardown @@ -182,7 +182,7 @@ makes use of asynchronous generators:: Sometimes you may want the cleanup to know whether the context was ended because of an unhandled exception. The one use that has come up so far is committing or rolling back a database transaction. This can be achieved by passing the ``pass_exception`` keyword argument to -:meth:`~asphalt.core.context.Context.add_teardown_callback`:: +:meth:`Context.add_teardown_callback`:: class FooComponent(Component): async def start(ctx): @@ -197,7 +197,7 @@ transaction. This can be achieved by passing the ``pass_exception`` keyword argu ctx.add_teardown_callback(teardown, pass_exception=True) ctx.add_resource(db) -The same can be achieved with :func:`~asphalt.core.context.context_teardown` by storing the yielded +The same can be achieved with :func:`context_teardown` by storing the yielded value:: class FooComponent(Component): diff --git a/docs/userguide/deployment.rst b/docs/userguide/deployment.rst index a67147cc..56021141 100644 --- a/docs/userguide/deployment.rst +++ b/docs/userguide/deployment.rst @@ -1,6 +1,8 @@ Configuration and deployment ============================ +.. py:currentmodule:: asphalt.core + As your application grows more complex, you may find that you need to have different settings for your development environment and your production environment. You may even have multiple deployments that all need their own custom configuration. @@ -29,9 +31,9 @@ What this will do is: #. read all the given configuration files, if any, starting from ``yourconfig.yaml`` #. read the command line configuration options passed with ``--set``, if any -#. merge the configuration files' contents and the command line configuration options into a single configuration dictionary using - :func:`~asphalt.core.utils.merge_config`. -#. call :func:`~asphalt.core.runner.run_application` using the configuration dictionary as keyword +#. merge the configuration files' contents and the command line configuration options + into a single configuration dictionary using :func:`merge_config`. +#. call :func:`run_application` using the configuration dictionary as keyword arguments Writing a configuration file @@ -86,12 +88,12 @@ You could then write a configuration file like this:: In the above configuration you have three top level configuration keys: ``max_threads``, ``component`` and ``logging``, all of which are directly passed to -:func:`~asphalt.core.runner.run_application` as keyword arguments. +:func:`run_application` as keyword arguments. The ``component`` section defines the type of the root component using the specially processed ``type`` option. You can either specify a setuptools entry point name (from the ``asphalt.components`` namespace) or a text reference like ``module:class`` (see -:func:`~asphalt.core.utils.resolve_reference` for details). The rest of the keys in this section are +:func:`resolve_reference` for details). The rest of the keys in this section are passed directly to the constructor of the ``MyRootComponent`` class. The ``components`` section within ``component`` is processed in a similar fashion. @@ -146,7 +148,7 @@ Configuration overlays Component configuration can be specified on several levels: -* Hard-coded arguments to :meth:`~asphalt.core.component.ContainerComponent.add_component` +* Hard-coded arguments to :meth:`ContainerComponent.add_component` * First configuration file argument to ``asphalt run`` * Second configuration file argument to ``asphalt run`` * ... diff --git a/docs/userguide/events.rst b/docs/userguide/events.rst index a3f32a84..f75d13a3 100644 --- a/docs/userguide/events.rst +++ b/docs/userguide/events.rst @@ -1,19 +1,21 @@ Working with signals and events =============================== +.. py:currentmodule:: asphalt.core + Events are a handy way to make your code react to changes in another part of the application. To dispatch and listen to events, you first need to have one or more -:class:`~asphalt.core.event.Signal` instances as attributes of some class. Each signal needs to be -associated with some :class:`~asphalt.core.event.Event` class. Then, when you dispatch a new event -by calling :meth:`~asphalt.core.event.Signal.dispatch`, a new instance of this event class will be +:class:`Signal` instances as attributes of some class. Each signal needs to be +associated with some :class:`Event` class. Then, when you dispatch a new event +by calling :meth:`Signal.dispatch`, a new instance of this event class will be constructed and passed to all listener callbacks. To listen to events dispatched from a signal, you need to have a function or any other callable that accepts a single positional argument. You then pass this callable to -:meth:`~asphalt.core.event.Signal.connect`. That's it! +:meth:`Signal.connect`. That's it! -To disconnect the callback, simply call :meth:`~asphalt.core.event.Signal.disconnect` with whatever -you passed to :meth:`~asphalt.core.event.Signal.connect` as argument. +To disconnect the callback, simply call :meth:`Signal.disconnect` with whatever +you passed to :meth:`Signal.connect` as argument. Here's how it works:: @@ -54,7 +56,7 @@ Exception handling ------------------ Any exceptions raised by the listener callbacks are logged to the ``asphalt.core.event`` logger. -Additionally, the future returned by :meth:`~asphalt.core.event.Signal.dispatch` resolves to +Additionally, the future returned by :meth:`Signal.dispatch` resolves to ``True`` if no exceptions were raised during the processing of listeners. This was meant as a convenience for use with tests where you can just do ``assert await thing.some_signal.dispatch('foo')``. @@ -63,14 +65,14 @@ Waiting for a single event -------------------------- To wait for the next event dispatched from a given signal, you can use the -:meth:`~asphalt.core.event.Signal.wait_event` method:: +:meth:`Signal.wait_event` method:: async def print_next_event(source): event = await source.somesignal.wait_event() print(event) You can even wait for the next event dispatched from any of several signals using the -:func:`~asphalt.core.event.wait_event` function:: +:func:`wait_event` function:: from asphalt.core import wait_event @@ -90,7 +92,7 @@ the callback returns ``True``:: Receiving events iteratively ---------------------------- -With :meth:`~asphalt.core.event.Signal.stream_events`, you can even asynchronously iterate over +With :meth:`Signal.stream_events`, you can even asynchronously iterate over events dispatched from a signal:: from contextlib import aclosing # on Python < 3.10, import from async_generator or contextlib2 @@ -101,7 +103,7 @@ events dispatched from a signal:: async for event in stream: print(event) -Using :func:`~asphalt.core.event.stream_events`, you can stream events from multiple signals:: +Using :func:`stream_events`, you can stream events from multiple signals:: from asphalt.core import stream_events @@ -112,7 +114,7 @@ Using :func:`~asphalt.core.event.stream_events`, you can stream events from mult async for event in stream: print(event) -The filtering capability of :func:`~asphalt.core.event.wait_event` works here too:: +The filtering capability of :func:`wait_event` works here too:: async def listen_to_events(source1, source2, source3): stream = stream_events(source1.some_signal, source2.another_signal, source3.some_signal, diff --git a/docs/userguide/testing.rst b/docs/userguide/testing.rst index 45f7a0e1..60309853 100644 --- a/docs/userguide/testing.rst +++ b/docs/userguide/testing.rst @@ -28,19 +28,20 @@ Create a ``tests`` directory at the root of the project directory and create a m import pytest from asphalt.core import Context + from pytest import CaptureFixture from echo.client import ClientComponent from echo.server import ServerComponent - def test_client_and_server(event_loop, capsys): + async def test_client_and_server(capsys: CaptureFixture[str]) -> None: async def run(): - async with Context() as ctx: + async with Context(): server = ServerComponent() - await server.start(ctx) + await server.start() client = ClientComponent("Hello!") - await client.start(ctx) + await client.start() event_loop.create_task(run()) with pytest.raises(SystemExit) as exc: @@ -52,11 +53,9 @@ Create a ``tests`` directory at the root of the project directory and create a m out, err = capsys.readouterr() assert out == "Message from client: Hello!\nServer responded: Hello!\n" -The test module above contains one test function which uses two fixtures: - -* ``event_loop``: comes from pytest-asyncio_; provides an asyncio event loop -* ``capsys``: captures standard output and error, letting us find out what message the components - printed +The test module above contains one test function which uses one fixture (``capsys``). +This fixture is provided by ``pytest``, and it captures standard output and error, +letting us find out what message the components printed. In the test function (``test_client_and_server()``), the server and client components are instantiated and started. Since the client component's