Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 17 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ Package structure and layer rules: [docs/architecture.md](docs/architecture.md).

## Upgrading to Winter 34

An application is composed explicitly in a `winter.application.Application`, which imports every module of
the given packages on construction (by default, those of the directory of the module that creates it), and
adapters build from it. `find_package_routes`, `create_django_urls_from_routes`, `WinterApplication` and the
`setup()` functions of `winter.web`, `winter.django` and `winter.openapi` are gone.
A web application is composed explicitly in a `winter.web.WebApplication`, which imports every module of the
given packages and their subpackages on construction, and adapters build from it. `find_package_routes`,
`create_django_urls_from_routes`, `WinterApplication` and the `setup()` functions of `winter.web`, `winter.django`
and `winter.openapi` are gone.

```python
import service_a
import winter.django
from winter.application import Application
import winter.web

app = Application(modules=[service_a], injector=injector) # several applications may share one injector
app = winter.web.WebApplication(modules=[service_a], injector=injector) # applications may share one injector
urlpatterns = winter.django.create_urls(app)
```

Expand Down Expand Up @@ -64,6 +64,9 @@ urlpatterns = winter.django.create_urls(app)
startup instead of on the first request.
- An argument named `response_headers` of an exception handler no longer receives the headers of the response:
declare a `ResponseHeader[T]` with `@winter.web.response_header`, as in an API method.
- Interceptors belong to applications: `WebApplication(..., interceptors=[AuthorizationInterceptor, AuditInterceptor])`
lists their classes in the order they run. The injector of each request creates them, so their dependencies go
to `__init__`. `interceptor_registry` and `InterceptorRegistry` are gone.
- The request pipeline takes the injector from the application. `winter.application.set_injector` is needed only
by `winter.sqlalchemy`, `winter.ddd` and `winter.messaging`.
- `winter.django.create_urls` installs the Django throttling identity on its first call: replace it with
Expand Down Expand Up @@ -125,9 +128,9 @@ To use it with Django, in `urls.py`:
```python
import some_package
import winter.django
from winter.application import Application
import winter.web

app = Application(modules=[some_package])
app = winter.web.WebApplication(modules=[some_package])
urlpatterns = [
*winter.django.create_urls(app),
]
Expand Down Expand Up @@ -316,7 +319,8 @@ class TodoProblemExistsExampleAPI:

## Interceptors
You can define interceptors to pre-handle a web request before it gets to an endpoint code.
The pre_handle method arguments will be injected the same way as it's done in methods with winter.web.route annotation.
The pre_handle method arguments are resolved the same way as those of an API method, and `method` receives the API
method of the request.
It's not supported to return any response from interceptors.
However, the exceptions thrown from within an interceptor will be handled automatically.
```python
Expand All @@ -336,19 +340,10 @@ class HelloWorldInterceptor(Interceptor):

```


The only way now to register an interceptor is to add it to InterceptorRegistry singleton.

Don't forget to import this adding during app initialization, before the URL patterns are created: they check
the arguments of the registered interceptors on startup.
An application lists the classes of its interceptors in the order they run. The injector of each request creates
them, so their dependencies go to `__init__`:
```python
from winter.web import interceptor_registry
from .interceptors import HelloWorldInterceptor

class SomeInitializationClassInMyApplication:
def initialize_my_app(self):
interceptor_registry.add_interceptor(HelloWorldInterceptor())

app = winter.web.WebApplication(modules=[some_package], interceptors=[HelloWorldInterceptor])
```

## Values from the injector
Expand All @@ -362,7 +357,6 @@ Django, `django.http.HttpRequest`.
import injector
import winter.web
from django.http import HttpRequest
from winter.application import Application


class AccountModule(injector.Module):
Expand All @@ -382,7 +376,7 @@ class ProfileAPI:
return self._profiles.get(account)


app = Application(modules=[profiles], injector=injector.Injector([AccountModule()]))
app = winter.web.WebApplication(modules=[profiles], injector=injector.Injector([AccountModule()]))
```

Interceptors and exception handlers of a request get the same request-scoped values as its API method.
Expand Down
26 changes: 14 additions & 12 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ independent of each other. Dashed packages are planned and do not exist yet.
|---|---|---|
| Adapters | Bind the models to external technologies and standards. Independent of each other. | `django`, `openapi`, `sqlalchemy`, `redis`, `wsgi` (planned), `asgi` (planned), `rabbitmq` (planned) |
| Models | Framework-agnostic descriptions of what an application does. Independent of each other. | `web` (HTTP), `ddd` (domain: aggregates, repositories, domain events), `messaging` (events) |
| Foundations | Shared vocabulary and mechanisms. Unaware of the layers above. May use each other. | `components` (declarative metadata), `application` (scope, injector, discovery), `json`, `pagination`, `typing` (what the stdlib `typing` lacks) |
| Foundations | Shared vocabulary and mechanisms. Unaware of the layers above. May use each other. | `components` (declarative metadata), `application` (discovery of definitions, injector), `json`, `pagination`, `typing` (what the stdlib `typing` lacks) |

A package belongs to Foundations when it knows nothing about models or adapters and is used by
at least two of them.
Expand All @@ -69,8 +69,9 @@ at least two of them.
import path changing.
2. **Framework behaviour is never process-global.** Every behavioural registry (exception
handlers, interceptors, output processors, throttling configuration, OpenAPI inspectors)
belongs to an `Application` instance, and the values that its methods receive come from its
injector, so two applications can live in one process.
belongs to the application object an adapter builds from, such as `winter.web.WebApplication`,
and the values that its methods receive come from its injector, so two applications can live
in one process.
Declarative metadata about types (component annotations, JSON encoders for a type) may be
global, like the classes it describes.
3. **Glue between models, or between adapters, is application code.** Mapping a domain
Expand All @@ -81,18 +82,18 @@ at least two of them.

## Current deviations

- **Framework behaviour is still process-global.** An `Application` holds the code of an
application and its injector. The adapters derive its exception handlers from its definitions and
handle each of its requests in a child of its injector. The other behavioural registries of
`winter.web` and `winter.openapi` (interceptors, output processors, response header serializers,
native response types, throttling, OpenAPI inspectors) are module singletons, and
- **Framework behaviour is still process-global.** A `winter.web.WebApplication` holds the code of
a web application, its injector and its interceptors. The adapters derive its exception handlers
from its definitions and handle each of its requests in a child of its injector. The other
behavioural registries of `winter.web` and `winter.openapi` (output processors, response header
serializers, native response types, throttling, OpenAPI inspectors) are module singletons, and
`winter.sqlalchemy`, `winter.ddd` and `winter.messaging` take the injector from the service
locator `winter.application.get_injector`.
- **Models are initialised once per process.** `winter.web.initialization.initialize`, the Django
adapter's `_initialize_process` and `winter.openapi.initialization.initialize` fill those module
singletons with the standard output processors, serializers and inspectors on first use. In the
target, each `Application` owns these registries in an injector of its own, a child of the injector
it is given, and they start with the standard entries. Building from an application, an adapter adds
target, a `WebApplication` is given these registries like its interceptors, and they start with the
standard entries. Building from an application, an adapter adds
its own entries (the native response type, the throttling identity) to that application only. The
`initialize` functions then disappear.
- **`winter.wsgi`, `winter.asgi` and `winter.rabbitmq` do not exist yet.** HTTP is served through
Expand All @@ -108,7 +109,8 @@ at least two of them.
There are no `ignore_imports`, and none may be added.
- **Isolation test** `tests/test_application_scope.py` builds two applications in one process
and asserts that the handler of a `@problem` exception of one does not handle that exception in
the other, and that the arguments of an API method come from the injector of the application
that serves it. Each registry that moves into `Application` adds its case there.
the other, that the arguments of an API method come from the injector of the application that
serves it, and that an interceptor of one does not run in the other. Each registry that moves into
the application adds its case there.
- **This document** describes intent and is updated when the intent changes, not when the code
moves. Deviations are listed above while they exist.
13 changes: 0 additions & 13 deletions tests/application/caller_directory/app.py

This file was deleted.

115 changes: 0 additions & 115 deletions tests/application/test_application.py

This file was deleted.

67 changes: 67 additions & 0 deletions tests/application/test_definitions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import pytest

from winter.application import find_definitions


def test_definitions_are_classes_and_functions_defined_in_modules():
import tests.application.scanned.api
from tests.application.scanned.api import API
from tests.application.scanned.api import function

# Act
definitions = find_definitions([tests.application.scanned.api])

# Assert
assert definitions == (API, function)


def test_package_is_scanned_with_its_submodules():
import tests.application.scanned.subpackage
from tests.application.scanned.subpackage.deep import Deep

# Act
definitions = find_definitions([tests.application.scanned.subpackage])

# Assert
assert definitions == (Deep,)


def test_directory_without_init_is_not_scanned():
import tests.application.scanned
from tests.application.scanned.api import API
from tests.application.scanned.api import function
from tests.application.scanned.subpackage.deep import Deep

# Act
definitions = find_definitions([tests.application.scanned])

# Assert
assert definitions == (API, function, Deep)


def test_definition_of_module_given_twice_is_found_once():
import tests.application.scanned
import tests.application.scanned.subpackage
from tests.application.scanned.api import API
from tests.application.scanned.api import function
from tests.application.scanned.subpackage.deep import Deep

# Act
definitions = find_definitions([tests.application.scanned, tests.application.scanned.subpackage])

# Assert
assert definitions == (API, function, Deep)


def test_unimportable_module_fails():
import tests.application.unimportable

# Act
with pytest.raises(RuntimeError, match='Unimportable') as exception_info:
find_definitions([tests.application.unimportable])

# Assert
assert exception_info.value.__notes__ == [
'Finding the definitions imports every module of tests.application.unimportable, '
'so each of them must be importable',
]
5 changes: 0 additions & 5 deletions tests/apps.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
from django.apps import AppConfig

from tests.web.interceptors import HelloWorldInterceptor
import winter.web
from winter.web import InMemoryThrottlingStorage
from winter.web import interceptor_registry


class TestAppConfig(AppConfig):
name = 'tests'

def ready(self):
# Before the URL patterns, which check the arguments of the interceptors on startup
interceptor_registry.add_interceptor(HelloWorldInterceptor())

# Importing the URLs creates the application and its URL patterns
from .urls import urlpatterns # noqa: F401

Expand Down
3 changes: 1 addition & 2 deletions tests/openapi/test_exception_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import winter.openapi
import winter.web
from winter.application import Application
from winter.application import get_injector
from winter.web.routing import get_route
from winter.openapi.generator import generate_openapi_for_routes
Expand Down Expand Up @@ -106,7 +105,7 @@ class _TestAPI:
def get_resource(self): # pragma: no cover
pass

app = Application(modules=[], injector=get_injector())
app = winter.web.WebApplication(modules=[], injector=get_injector())
app.add(_Handlers)
app.add(_TestAPI)

Expand Down
Loading
Loading