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]