diff --git a/documentation/docs/library_services/ciphertext.mdx b/documentation/docs/library_services/ciphertext.mdx
index c229bdfd6..42452dab7 100644
--- a/documentation/docs/library_services/ciphertext.mdx
+++ b/documentation/docs/library_services/ciphertext.mdx
@@ -54,8 +54,9 @@ application — you do not need to set it yourself.
## Imports and servicers
-The `ciphertext` library depends on [`OrderedMap`](./ordered_map), so include
-both when starting your `Application`.
+The `ciphertext` library depends on [`OrderedMap`](./ordered_map). In
+Python, listing `ciphertext_library()` brings it along; in TypeScript,
+list both.
@@ -63,14 +64,11 @@ both when starting your `Application`.
```py
from reboot.std.ciphertext.v1.ciphertext import ciphertext_library
-from reboot.std.collections.ordered_map.v1.ordered_map import (
- ordered_map_library,
-)
async def main():
application = Application(
servicers=[MyServicer],
- libraries=[ciphertext_library(), ordered_map_library()],
+ libraries=[ciphertext_library()],
)
await application.run()
```
diff --git a/documentation/docs/library_services/oauth_token_manager.mdx b/documentation/docs/library_services/oauth_token_manager.mdx
index 11182e4a8..3af204802 100644
--- a/documentation/docs/library_services/oauth_token_manager.mdx
+++ b/documentation/docs/library_services/oauth_token_manager.mdx
@@ -38,28 +38,20 @@ service without one (e.g. `"slack.com"`).
## Imports and servicers
-The `oauth` library builds on [`Ciphertext`](./ciphertext) (which builds
-on [`OrderedMap`](./ordered_map)), so include all three when starting
-your `Application`; omitting any of them fails fast at startup.
+The `oauth` library builds on [`Ciphertext`](./ciphertext), which builds
+on [`OrderedMap`](./ordered_map). In Python, listing `oauth_library()`
+brings both along; in TypeScript, list all three.
```py
from reboot.std.oauth.v1.oauth import oauth_library
-from reboot.std.ciphertext.v1.ciphertext import ciphertext_library
-from reboot.std.collections.ordered_map.v1.ordered_map import (
- ordered_map_library,
-)
async def main():
application = Application(
servicers=[MyServicer],
- libraries=[
- oauth_library(),
- ciphertext_library(),
- ordered_map_library(),
- ],
+ libraries=[oauth_library()],
)
await application.run()
```
diff --git a/documentation/docs/library_services/overview.md b/documentation/docs/library_services/overview.md
index 368d55f74..2f6cd3517 100644
--- a/documentation/docs/library_services/overview.md
+++ b/documentation/docs/library_services/overview.md
@@ -57,6 +57,11 @@ new Application({
+In Python, a library's own requirements come along automatically:
+listing `queue_library()` also mounts the `SortedMap` library it builds
+on. List a required library yourself only to customize it, for example
+with an authorizer.
+
## Future libraries and integrations
[Reach out to us](https://discord.gg/cRbdcS94Nr) if there are any integrations or standard library features you want us to prioritize. Or we can help you build them, if that's more your speed!
diff --git a/documentation/docs/library_services/pubsub.mdx b/documentation/docs/library_services/pubsub.mdx
index a3e13cce9..a26f4e40f 100644
--- a/documentation/docs/library_services/pubsub.mdx
+++ b/documentation/docs/library_services/pubsub.mdx
@@ -55,25 +55,21 @@ import { Topic } from "@reboot-dev/reboot-std/pubsub/v1";
-Also make sure to include the pub/sub library and the dependent
-[`Queue`](./queue) and [`SortedMap`](./sorted_map) libraries when starting up
-your `Application`. (Note: this import is different from above.)
+Also make sure to include the pub/sub library when starting up your
+`Application`. In Python, listing `pubsub_library()` brings along the
+[`Queue`](./queue) and [`SortedMap`](./sorted_map) libraries it depends
+on; in TypeScript, list all three. (Note: this import is different from
+above.)
```py
-from reboot.std.collections.queue.v1.queue import queue_library
-from reboot.std.collections.v1.sorted_map import sorted_map_library
from reboot.std.pubsub.v1.pubsub import pubsub_library
async def main():
application = Application(
servicers=[MyServicer],
- libraries=[
- pubsub_library(),
- queue_library(),
- sorted_map_library(),
- ],
+ libraries=[pubsub_library()],
)
await application.run()
```
diff --git a/documentation/docs/library_services/queue.mdx b/documentation/docs/library_services/queue.mdx
index 673f198a6..823ec2533 100644
--- a/documentation/docs/library_services/queue.mdx
+++ b/documentation/docs/library_services/queue.mdx
@@ -46,20 +46,20 @@ import { Queue } from "@reboot-dev/reboot-std/collections/queue/v1";
-Also make sure to include the `Queue` library and the dependent [`SortedMap`](./sorted_map)
-library when starting up your `Application`. (Note: this import is different
-from above.)
+Also make sure to include the `Queue` library when starting up your
+`Application`. In Python, listing `queue_library()` brings along the
+[`SortedMap`](./sorted_map) library it depends on; in TypeScript, list
+both. (Note: this import is different from above.)
```py
from reboot.std.collections.queue.v1.queue import queue_library
-from reboot.std.collections.v1.sorted_map import sorted_map_library
async def main():
application = Application(
servicers=[MyServicer],
- libraries=[queue_library(), sorted_map_library()],
+ libraries=[queue_library()],
)
await application.run()
```
diff --git a/reboot/aio/applications.py b/reboot/aio/applications.py
index f00c6e19a..4403a76ba 100644
--- a/reboot/aio/applications.py
+++ b/reboot/aio/applications.py
@@ -65,7 +65,15 @@
from reboot.version import REBOOT_VERSION
from reboot.versioning import version_less_than
from starlette.staticfiles import StaticFiles
-from typing import Any, Awaitable, Callable, Mapping, NoReturn, Optional
+from typing import (
+ Any,
+ Awaitable,
+ Callable,
+ ClassVar,
+ Mapping,
+ NoReturn,
+ Optional,
+)
logger = get_logger(__name__)
@@ -184,6 +192,12 @@ class Library(AbstractLibrary):
any checks on subclasses.
"""
+ # Every library class by its `name`, so that a library's
+ # `requirements()` can be constructed without the application
+ # listing them. A class defined later under the same name replaces
+ # the earlier one, which keeps reloading a module harmless.
+ _registry: ClassVar[dict[str, type['Library']]] = {}
+
def __init_subclass__(cls, **kwargs):
# `name` checked here because this class expects its subclasses
# to define it.
@@ -193,6 +207,7 @@ def __init_subclass__(cls, **kwargs):
"Please set `name` as a class variable. For best practices, "
"please define the name as a constant in your library module."
)
+ Library._registry[cls.name] = cls
async def pre_run(self, application: Application) -> None:
"""
@@ -231,6 +246,25 @@ async def initialize(self, context: InitializeContext) -> None:
await self._initialize(context)
+def _construct_library(name: str, required_by: str) -> Library:
+ """Constructs, with its defaults, the library registered as `name`."""
+ library_type = Library._registry.get(name)
+ if library_type is None:
+ raise ValueError(
+ f"Library `{required_by}` requires library `{name}`, which is "
+ "not one Reboot can construct itself. Please construct it and "
+ "pass it in the `libraries` parameter."
+ )
+ try:
+ return library_type()
+ except TypeError as error:
+ raise ValueError(
+ f"Library `{required_by}` requires library `{name}`, which can "
+ "not be constructed without arguments. Please construct it and "
+ "pass it in the `libraries` parameter."
+ ) from error
+
+
class Application:
"""Entry point for all Reboot applications."""
@@ -258,7 +292,10 @@ def __init__(
:param legacy_grpc_servicers: the types of legacy gRPC servicers
(not using Reboot libraries) that this Application will
serve.
- :param libraries: the libraries this Application will use.
+ :param libraries: the libraries this Application will use. A
+ library's own requirements are added automatically; list a
+ required library yourself only to customize it, e.g. with
+ an authorizer.
:param initialize: will be called after the Application's
servicers have started for the first time, so that it can
perform initialization logic (e.g., creating some well-known
@@ -321,23 +358,30 @@ def __init__(
"library once in `libraries`."
)
- # Verify we have all the requirements for libraries.
- requirements_names = set(
- requirement for library in libraries
- for requirement in library.requirements()
- )
- needed_requirements = requirements_names - library_names
-
- if len(needed_requirements) > 0:
- raise ValueError(
- "Missing required libraries: "
- f"{', '.join(needed_requirements)}. "
- "Please add these libraries and pass them to the "
- "`libraries` parameter."
- )
+ # Add each library's requirements, and theirs in turn, so
+ # that listing a library is enough to run it. A library the
+ # application lists itself takes precedence over one
+ # constructed here, which is how an application customizes
+ # a dependency, e.g. with an authorizer.
+ libraries_by_name = {
+ library.name: library for library in libraries
+ }
+ pending = list(libraries)
+ while len(pending) > 0:
+ library = pending.pop()
+ for requirement in library.requirements():
+ if requirement in libraries_by_name:
+ continue
+ required = _construct_library(
+ requirement, required_by=library.name
+ )
+ libraries_by_name[requirement] = required
+ pending.append(required)
# Sort libraries by name for guaranteed ordering.
- libraries = sorted(libraries, key=lambda library: library.name)
+ libraries = sorted(
+ libraries_by_name.values(), key=lambda library: library.name
+ )
# Add the library servicers to the list of servicers.
library_servicers = [
@@ -958,27 +1002,26 @@ def _frontend_project_root(self) -> Path:
def _require_oauth_libraries(self) -> None:
"""Fail fast if an OAuth provider with `store_tokens=True` is used
- without the `oauth` (and its `ciphertext`) library mounted — they
- encrypt and persist the identity provider's tokens.
+ without the `oauth` library mounted — it (with the `ciphertext`
+ library it builds on) encrypts and persists the identity
+ provider's tokens.
"""
- # Imported lazily: both libraries import `reboot.aio.applications`,
+ # Imported lazily: the library imports `reboot.aio.applications`,
# so a module-level import would be circular.
- from reboot.std.ciphertext.v1.ciphertext import CIPHERTEXT_LIBRARY_NAME
from reboot.std.oauth.v1.oauth import OAUTH_LIBRARY_NAME
names = {library.name for library in (self._libraries or [])}
- if OAUTH_LIBRARY_NAME in names and CIPHERTEXT_LIBRARY_NAME in names:
+ if OAUTH_LIBRARY_NAME in names:
return
raise InputError(
reason=(
"An OAuth provider with `store_tokens=True` needs the "
- "`oauth` and `ciphertext` libraries to encrypt and persist "
- "the identity provider's tokens, but they aren't all "
- "mounted. Add them to your `Application`, e.g. "
- "`Application(..., libraries=[oauth_library(), "
- "ciphertext_library(), ordered_map_library()])` (import "
- "`oauth_library` from `reboot.std.oauth.v1.oauth` and "
- "`ciphertext_library` from "
- "`reboot.std.ciphertext.v1.ciphertext`)."
+ "`oauth` library to encrypt and persist the identity "
+ "provider's tokens, but it isn't mounted. Add it to your "
+ "`Application`, e.g. `Application(..., "
+ "libraries=[oauth_library()])` (import `oauth_library` "
+ "from `reboot.std.oauth.v1.oauth`); the `ciphertext` and "
+ "`ordered_map` libraries it builds on come along "
+ "automatically."
),
)
diff --git a/reboot/plugin/skills/mcp-ui/references/auth-store-tokens.md b/reboot/plugin/skills/mcp-ui/references/auth-store-tokens.md
index 3dc46d6bb..c6b0b0c6a 100644
--- a/reboot/plugin/skills/mcp-ui/references/auth-store-tokens.md
+++ b/reboot/plugin/skills/mcp-ui/references/auth-store-tokens.md
@@ -69,11 +69,10 @@ def _google() -> Google:
async def main() -> None:
application = Application(
servicers=[UserServicer],
- # `store_tokens=True` persists tokens via the `oauth` library,
- # which encrypts them via `ciphertext`, which in turn needs
- # `ordered_map`. Without all three the app fails fast at startup.
- libraries=[oauth_library(), ciphertext_library(),
- ordered_map_library()],
+ # `store_tokens=True` persists tokens via the `oauth` library;
+ # the `ciphertext` and `ordered_map` libraries it builds on come
+ # along automatically.
+ libraries=[oauth_library()],
oauth=OAuth(
provider=OAuthProviderByEnvironment(
# The calendar needs a real provider token even in
diff --git a/reboot/plugin/skills/python/references/auth-external-api-calls.md b/reboot/plugin/skills/python/references/auth-external-api-calls.md
index 2b756da95..c16a746eb 100644
--- a/reboot/plugin/skills/python/references/auth-external-api-calls.md
+++ b/reboot/plugin/skills/python/references/auth-external-api-calls.md
@@ -34,12 +34,9 @@ fails fast at startup:
```python
from reboot.std.oauth.v1.oauth import oauth_library
-from reboot.std.ciphertext.v1.ciphertext import ciphertext_library
-from reboot.std.collections.ordered_map.v1.ordered_map import (
- ordered_map_library,
-)
-# libraries=[oauth_library(), ciphertext_library(), ordered_map_library()]
+# libraries=[oauth_library()]; the `ciphertext` and `ordered_map`
+# libraries it builds on come along automatically.
```
`REBOOT_CRYPTO_ROOT_KEYS` backs the encryption; it is auto-provisioned
@@ -310,7 +307,7 @@ await KeyManager.ref(_key_manager_id(GOOGLE)).shred(context, scope=user_id)
## Checklist
-- [ ] `libraries=[oauth_library(), ciphertext_library(), ordered_map_library()]` on the `Application` (Path C needs only the latter two).
+- [ ] `libraries=[oauth_library()]` on the `Application` (Path C needs only `ciphertext_library()`).
- [ ] **Capture** — Path A (identity provider's own API):
`scopes=[...]` (least privilege) + `store_tokens=True`. Path B
(any other service): your own authorize + callback routes, callback registered `app_internal=True`, the
diff --git a/reboot/plugin/skills/python/references/lifecycle-application-entry.md b/reboot/plugin/skills/python/references/lifecycle-application-entry.md
index 2b333a272..1515f437a 100644
--- a/reboot/plugin/skills/python/references/lifecycle-application-entry.md
+++ b/reboot/plugin/skills/python/references/lifecycle-application-entry.md
@@ -99,18 +99,17 @@ async def main():
## See Also
-If you're using any stdlib state type, the wiring lives in **two**
-places: its `servicers()` list goes into `servicers=[...]`, and its
-`_library()` factory goes into `libraries=[...]`. Forgetting
-either gives a runtime "unknown actor type" error. The references for
-each type call out exactly what to register:
-
-- `stdlib-ordered-map.md` — `ordered_map.servicers()` + `ordered_map_library()`
-- `stdlib-queue.md` — `queue.servicers()` + the stdlib map library
- (`Queue` uses a stdlib sorted-map actor under the hood — see the
- reference for the exact import)
-- `stdlib-pubsub.md` — `pubsub.servicers()` (transitively pulls
- `queue.servicers()`) + the stdlib map library
+If you're using any stdlib state type, its `_library()` factory
+goes into `libraries=[...]`; that registers its servicers and mounts
+whatever the library itself depends on. Forgetting it gives a runtime
+"unknown actor type" error. The references for each type call out
+exactly what to register:
+
+- `stdlib-ordered-map.md` — `ordered_map_library()`
+- `stdlib-queue.md` — `queue_library()` (brings along the stdlib
+ sorted-map library `Queue` uses under the hood)
+- `stdlib-pubsub.md` — `pubsub_library()` (brings along `Queue` and
+ the sorted-map library)
- `stdlib-presence.md` — `presence.servicers()` (returns three
Servicers; no library factory)
diff --git a/reboot/plugin/skills/python/references/stdlib-ciphertext.md b/reboot/plugin/skills/python/references/stdlib-ciphertext.md
index 10d37b84b..13ef1e036 100644
--- a/reboot/plugin/skills/python/references/stdlib-ciphertext.md
+++ b/reboot/plugin/skills/python/references/stdlib-ciphertext.md
@@ -7,9 +7,10 @@ tags: stdlib, ciphertext, encryption, envelope, crypto-shred, gdpr, right-to-era
## Use `Ciphertext` for Envelope Encryption and Crypto-Shredding
-> **Critical:** register **both** `ciphertext_library()` **and** > `ordered_map_library()` in `Application(libraries=[...])` —
-> `Ciphertext` depends on `OrderedMap`; forgetting either fails at boot
-> with "unknown actor type." `associated_data` must be supplied
+> **Critical:** register `ciphertext_library()` in
+> `Application(libraries=[...])`; it brings along the `OrderedMap`
+> library it depends on. Forgetting it fails at boot with "unknown
+> actor type." `associated_data` must be supplied
> **byte-for-byte identical** at decrypt as at encrypt — build it with
> `make_associated_data`, never an ad-hoc string. These methods are
> **app-internal by default** (no authorizer); call them from within
@@ -47,15 +48,12 @@ root KEK — derived from REBOOT_CRYPTO_ROOT_KEYS (auto-provisioned); never st
```python
from reboot.std.ciphertext.v1.ciphertext import ciphertext_library
-from reboot.std.collections.ordered_map.v1.ordered_map import (
- ordered_map_library,
-)
async def main():
await Application(
servicers=[VaultServicer],
- libraries=[ciphertext_library(), ordered_map_library()],
+ libraries=[ciphertext_library()],
).run()
```
diff --git a/reboot/plugin/skills/python/references/stdlib-oauth-tokens.md b/reboot/plugin/skills/python/references/stdlib-oauth-tokens.md
index cf3db2a01..963f9bfeb 100644
--- a/reboot/plugin/skills/python/references/stdlib-oauth-tokens.md
+++ b/reboot/plugin/skills/python/references/stdlib-oauth-tokens.md
@@ -37,18 +37,14 @@ makes the app fail fast at boot:
```python
from reboot.std.oauth.v1.oauth import oauth_library
-from reboot.std.ciphertext.v1.ciphertext import ciphertext_library
-from reboot.std.collections.ordered_map.v1.ordered_map import (
- ordered_map_library,
-)
async def main():
await Application(
servicers=[...],
- libraries=[
- oauth_library(), ciphertext_library(), ordered_map_library(),
- ],
+ # The `ciphertext` and `ordered_map` libraries `oauth` builds on
+ # come along automatically.
+ libraries=[oauth_library()],
).run()
```
diff --git a/reboot/plugin/skills/python/references/stdlib-pubsub.md b/reboot/plugin/skills/python/references/stdlib-pubsub.md
index 5bdb6a52f..cc9369a1c 100644
--- a/reboot/plugin/skills/python/references/stdlib-pubsub.md
+++ b/reboot/plugin/skills/python/references/stdlib-pubsub.md
@@ -47,25 +47,23 @@ topic's buffer into each subscriber's queue.
### Register the Library
`Topic` is built on `Queue`, which is itself backed by an internal
-stdlib sorted-map actor:
+stdlib sorted-map actor. Registering `pubsub_library()` mounts all
+three:
```python
-from reboot.std.pubsub.v1 import pubsub
-from reboot.std.collections.v1.sorted_map import sorted_map_library
+from reboot.std.pubsub.v1.pubsub import pubsub_library
async def main():
await Application(
- servicers=[MyServicer] + pubsub.servicers(),
- libraries=[sorted_map_library()],
+ servicers=[MyServicer],
+ libraries=[pubsub_library()],
).run()
```
-`pubsub.servicers()` returns `[TopicServicer] + queue.servicers()`, so
-you don't need to add `queue.servicers()` separately. The
-`sorted_map_library()` registration is the only place you mention
-the backing sorted-map actor — for a user-facing sorted key/value
-collection, use `OrderedMap` (see `stdlib-ordered-map.md`).
+You should not reach for the backing queue or sorted-map actors'
+types directly in application code. For a user-facing sorted
+key/value collection, use `OrderedMap` (see `stdlib-ordered-map.md`).
### Subscribe a Queue to a Topic
diff --git a/reboot/plugin/skills/python/references/stdlib-queue.md b/reboot/plugin/skills/python/references/stdlib-queue.md
index f81b7747e..693e9e8d4 100644
--- a/reboot/plugin/skills/python/references/stdlib-queue.md
+++ b/reboot/plugin/skills/python/references/stdlib-queue.md
@@ -41,26 +41,24 @@ available). A `try_dequeue` exists for one-shot non-blocking pulls from
### Register the Library
-`Queue` is backed by an internal stdlib sorted-map actor — its
-servicers list pulls those in, and the matching library factory
-must be registered. Use the `queue.servicers()` helper:
+`Queue` is backed by an internal stdlib sorted-map actor. Registering
+`queue_library()` mounts the queue's servicers and the sorted-map
+library it depends on:
```python
-from reboot.std.collections.queue.v1 import queue
-from reboot.std.collections.v1.sorted_map import sorted_map_library
+from reboot.std.collections.queue.v1.queue import queue_library
async def main():
await Application(
- servicers=[MyServicer] + queue.servicers(),
- libraries=[sorted_map_library()],
+ servicers=[MyServicer],
+ libraries=[queue_library()],
).run()
```
-The `sorted_map_library()` registration is the only place you
-mention the backing sorted-map actor — you should not reach for
-its types directly in application code. For a user-facing sorted
-key/value collection, use `OrderedMap` (see `stdlib-ordered-map.md`).
+You should not reach for the backing sorted-map actor's types
+directly in application code. For a user-facing sorted key/value
+collection, use `OrderedMap` (see `stdlib-ordered-map.md`).
### Producer Pattern
diff --git a/tests/reboot/aio/libraries_test.py b/tests/reboot/aio/libraries_test.py
index 444550947..89cd51946 100644
--- a/tests/reboot/aio/libraries_test.py
+++ b/tests/reboot/aio/libraries_test.py
@@ -57,6 +57,22 @@ def requirements(self):
return [GREETER_LIBRARY_NAME]
+GREETER_4_LIBRARY_NAME = "tests.reboot.aio.libraries_test.Greeter4Library"
+
+
+class Greeter4Library(Library):
+ """A library with no servicers, requiring a library that itself
+ requires another."""
+
+ name = GREETER_4_LIBRARY_NAME
+
+ def servicers(self):
+ return []
+
+ def requirements(self):
+ return [GREETER_2_LIBRARY_NAME]
+
+
def greeter_library():
return GreeterLibrary()
@@ -69,6 +85,10 @@ def greeter3_library():
return Greeter3Library()
+def greeter4_library():
+ return Greeter4Library()
+
+
class TestCase(unittest.IsolatedAsyncioTestCase):
async def test_adds_servicers(self) -> None:
@@ -125,15 +145,99 @@ async def test_adds_dependent_library_servicers(self) -> None:
self.assertIn(Greeter2Servicer, application.servicers)
self.assertIn(Greeter3Servicer, application.servicers)
- async def test_throws_if_not_all_requirements_present(self) -> None:
+ async def test_adds_required_libraries(self) -> None:
+ application = Application(libraries=[greeter2_library()])
+
+ # The required library is added, with its servicers.
+ self.assertEqual(
+ {GreeterLibrary, Greeter2Library},
+ set(type(library) for library in application.libraries),
+ )
+ self.assertIn(MyGreeterServicer, application.servicers)
+ self.assertIn(Greeter2Servicer, application.servicers)
+
+ async def test_adds_required_libraries_transitively(self) -> None:
+ application = Application(libraries=[greeter4_library()])
+
+ self.assertEqual(
+ {GreeterLibrary, Greeter2Library, Greeter4Library},
+ set(type(library) for library in application.libraries),
+ )
+
+ async def test_keeps_listed_instance_of_required_library(self) -> None:
+ greeter = greeter_library()
+ application = Application(
+ libraries=[greeter2_library(), greeter],
+ )
+
+ # The listed instance is used rather than a fresh one.
+ self.assertEqual(2, len(application.libraries))
+ self.assertTrue(
+ any(library is greeter for library in application.libraries)
+ )
+
+ async def test_throws_if_requirement_unknown(self) -> None:
+
+ class NeedsUnknownLibrary(Library):
+
+ name = "tests.reboot.aio.libraries_test.NeedsUnknownLibrary"
+
+ def servicers(self):
+ return []
+
+ def requirements(self):
+ return ["tests.reboot.aio.libraries_test.Unknown"]
+
with self.assertRaises(ValueError) as error:
- Application(libraries=[greeter2_library()])
- self.assertEqual(type(error.exception), ValueError)
+ Application(libraries=[NeedsUnknownLibrary()])
self.assertIn(
- f"Missing required libraries: {GREETER_LIBRARY_NAME}. Please add these libraries and pass them to the `libraries` parameter.",
- str(error.exception)
+ "requires library `tests.reboot.aio.libraries_test.Unknown`, "
+ "which is not one Reboot can construct itself",
+ str(error.exception),
+ )
+
+ async def test_throws_if_requirement_needs_arguments(self) -> None:
+
+ class NeedsArgumentLibrary(Library):
+
+ name = "tests.reboot.aio.libraries_test.NeedsArgumentLibrary"
+
+ def __init__(self, argument: str):
+ self.argument = argument
+
+ def servicers(self):
+ return [MyGreeterServicer]
+
+ class RequiresNeedsArgumentLibrary(Library):
+
+ name = (
+ "tests.reboot.aio.libraries_test."
+ "RequiresNeedsArgumentLibrary"
+ )
+
+ def servicers(self):
+ return []
+
+ def requirements(self):
+ return [NeedsArgumentLibrary.name]
+
+ with self.assertRaises(ValueError) as error:
+ Application(libraries=[RequiresNeedsArgumentLibrary()])
+
+ self.assertIn(
+ "can not be constructed without arguments",
+ str(error.exception),
+ )
+
+ # Listing a constructed instance satisfies the requirement.
+ application = Application(
+ libraries=[
+ RequiresNeedsArgumentLibrary(),
+ NeedsArgumentLibrary("argument"),
+ ]
)
+ self.assertEqual(2, len(application.libraries))
async def test_require_class_name(self) -> None:
with self.assertRaises(NotImplementedError) as error:
@@ -155,7 +259,7 @@ async def test_initialize(self) -> None:
class Library1WithInitialize(Library):
"""Library with an initialize function."""
- name = "tests.reboot.aio.libraries_test.GreeterLibrary"
+ name = "tests.reboot.aio.libraries_test.Library1WithInitialize"
def servicers(self):
return [MyGreeterServicer]
@@ -167,7 +271,7 @@ async def initialize(self, context: InitializeContext) -> None:
class Library2WithInitialize(Library):
"""Library with an initialize function."""
- name = "tests.reboot.aio.libraries_test.Greeter2Library"
+ name = "tests.reboot.aio.libraries_test.Library2WithInitialize"
def servicers(self):
return [MyGreeterServicer]