Skip to content

Commit

Permalink
Doc polish
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed Jul 24, 2023
1 parent 88a40fd commit f812a5b
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 18 deletions.
57 changes: 40 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- begin-pypi -->

# A Service Registry for Dependency Injection
# A Service Locator for Python

> **Warning**
> ☠️ Not ready yet! ☠️
Expand All @@ -17,14 +17,16 @@ It provides you with a central place to register factories for types/interfaces

**This allows you to configure and manage resources in *one central place* and access them all in a *consistent* way.**

In practice that means that at runtime, you say, for example, "*Give me a database connection*!", and *svc-reg* will give you whatever you've configured it to return when asked for a database connection.
---

In practice that means that at runtime, you say "*Give me a database connection*!", and *svc-reg* will give you whatever you've configured it to return when asked for a database connection.
This can be an actual database connection or it can be a mock object for testing.

If you like the [*Dependency Inversion Principle*](https://en.wikipedia.org/wiki/Dependency_inversion_principle) (aka "*program against interfaces, not implementations*"), you would register concrete factories for abstract interfaces; in Python usually a [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol) or an [Abstract Base Class](https://docs.python.org/3.11/library/abc.html).

That:

- unifies **acquisition** and **cleanups**,
- unifies **acquisition** and **cleanups** of resources,
- simplifies **testing**,
- and allows for **easy health** checks across *all* resources.

Expand All @@ -51,14 +53,18 @@ The latter already works with [Flask](#flask).
You set it up like this:

```python
from sqlalchemy import Connection, create_engine

...

engine = create_engine("postgresql://localhost")

def engine_factory():
with engine.connect() as conn:
yield Database(conn)
yield conn

registry = svc_reg.Registry()
registry.register_factory(Database, engine_factory)
registry.register_factory(Connection, engine_factory)
```

The generator-based setup and cleanup may remind you of [Pytest fixtures](https://docs.pytest.org/en/stable/explanation/fixtures.html).
Expand All @@ -76,11 +82,14 @@ You're unlikely to use the core API directly, but knowing what's happening under

*svc-reg* has two essential concepts:


### Registries

A **`Registry`** allows to register factories for certain types.
It's expected to live as long as your application lives.
Its only job is to store and retrieve factories.

It is possible to register either factories or values:
It is possible to register either factory callables or values:

```python
>>> import svc_reg
Expand All @@ -96,9 +105,10 @@ It is possible to register either factories or values:
The values and return values of the factories don't have to be actual instances of the type they're registered for.
But the types must be *hashable* because they're used as keys in a lookup dictionary.

---

A **`Container`** belongs to a Registry and allows to create instances of the registered types and takes care of their life-cycle:
### Containers

A **`Container`** belongs to a Registry and allows to create instances of the registered types, taking care of their life-cycle:

```python
>>> container = svc_reg.Container(reg)
Expand All @@ -117,6 +127,9 @@ True

A container lives as long as you want the instances to live -- e.g., as long as a request lives.


#### Cleanup

If a factory is a generator and yields the instance, the generator will be remembered.
At the end, you run `container.close()` and all generators will be finished (i.e. called `next(factory)` again).
You can use this to return database connections to a pool, et cetera.
Expand All @@ -125,7 +138,12 @@ If you have async generators, use `await container.aclose()` instead which calls

Failing cleanups are logged at `warning` level but otherwise ignored.

---
**The key idea is that your business code doesn't have to care about cleaning up resources it has requested.**

That makes it even easier to test it because the business codes makes fewer assumptions about the object it's getting.


#### Health Checks

Additionally, each registered service may have a `ping` callable that you can use for health checks.
You can request all pingable registered services with `container.get_pings()`.
Expand All @@ -140,7 +158,7 @@ The Flask integration takes care of this for you.

How to achieve this in other frameworks elegantly is TBD.

---
### Summary

Generally, the `Registry` object should live on an application-scoped object like Flask's `app.config` object.
On the other hand, the `Container` object should live on a request-scoped object like Flask's `g` object or Pyramid's `request` object.
Expand Down Expand Up @@ -178,14 +196,15 @@ import svc_reg

def create_app(config_filename):
app = Flask(__name__)
app.config.from_pyfile(config_filename)

...

##########################################################################
# Set up the registry using Flask integration.
app = svc_reg.flask.init_app(app)

# Now, register a factory that calls `engine.connect()` if you ask for a
# Connections. Since we use yield inside of a context manager, the
# `Connection`. Since we use yield inside of a context manager, the
# connection gets cleaned up when the container is closed.
# If you ask for a ping, it will run `SELECT 1` on a new connection and
# clean up the connection behind itself.
Expand Down Expand Up @@ -214,18 +233,15 @@ def create_app(config_filename):
)
##########################################################################

from yourapplication.views.admin import admin
from yourapplication.views.frontend import frontend
app.register_blueprint(admin)
app.register_blueprint(frontend)
...

return app
```

Now you can request the `Connection` object in your views:

```python
@app.route("/")
@app.get("/")
def index() -> flask.ResponseValue:
conn: Connection = svc_reg.flask.get(Connection)
```
Expand Down Expand Up @@ -352,6 +368,13 @@ Therefore it returns `Any`, and until Mypy changes its stance, you have to use i
conn: Connection = container.get(Connection)
```

If types are more important to you than a unified interface, you can always wrap it:

```python
def get_conn(container: reg_svc.Container) -> Connection:
return container.get(Connection)
```


## Credits

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
dynamic = ["version", "readme"]
name = "svc-reg"
description = "A Service Registry for Dependency Injection"
description = "A Service Locator for Python"
requires-python = ">=3.8"
license = "MIT"
keywords = ["dependency injection"]
Expand Down

0 comments on commit f812a5b

Please sign in to comment.