From be035ba4d339443d1c970a14412b0a17c91b456e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 6 Nov 2023 07:46:22 -0500 Subject: [PATCH] docs: add documentation (#77) * docs: add docs * style(pre-commit.ci): auto fixes [...] * move file * check manifest * force * add link --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .readthedocs.yaml | 18 +++ docs/css/style.css | 170 +++++++++++++++++++++ docs/gen_ref_nav.py | 37 +++++ docs/getting_started.md | 254 +++++++++++++++++++++++++++++++ docs/index.md | 40 +++++ mkdocs.yml | 77 ++++++++++ pyproject.toml | 14 +- src/in_n_out/__init__.py | 19 ++- src/in_n_out/_global.py | 44 ++++++ src/in_n_out/_store.py | 74 +++++---- src/in_n_out/_type_resolution.py | 41 ++--- 11 files changed, 724 insertions(+), 64 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 docs/css/style.css create mode 100644 docs/gen_ref_nav.py create mode 100644 docs/getting_started.md create mode 100644 docs/index.md create mode 100644 mkdocs.yml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..930f6fa --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,18 @@ +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.10" + +mkdocs: + configuration: mkdocs.yml + fail_on_warning: true + +python: + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/docs/css/style.css b/docs/css/style.css new file mode 100644 index 0000000..e77166b --- /dev/null +++ b/docs/css/style.css @@ -0,0 +1,170 @@ +/* Increase logo size */ +.md-header__button.md-logo { + padding-bottom: 0.2rem; + padding-right: 0; +} +.md-header__button.md-logo img { + height: 1.5rem; +} + +/* Mark external links as such (also in nav) */ +a.external:hover::after, a.md-nav__link[href^="https:"]:hover::after { + /* https://primer.style/octicons/link-external-16 */ + background-image: url('data:image/svg+xml,'); + height: 0.8em; + width: 0.8em; + margin-left: 0.2em; + content: ' '; + display: inline-block; +} + +/* More space at the bottom of the page */ +.md-main__inner { + margin-bottom: 1.5rem; +} + +/* ------------------------------- */ + +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: .05rem solid var(--md-typeset-table-color); +} + +/* Mark external links as such. */ +a.external::after, +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + mask-image: url('data:image/svg+xml,'); + -webkit-mask-image: url('data:image/svg+xml,'); + content: ' '; + + display: inline-block; + vertical-align: middle; + position: relative; + + height: 1em; + width: 1em; + background-color: var(--md-typeset-a-color); +} + +a.external:hover::after, +a.autorefs-external:hover::after { + background-color: var(--md-accent-fg-color); +} + +/* Avoid breaking parameters name, etc. in table cells. */ +td code { + word-break: normal !important; +} + +/* ------------------------------- */ + +/* Avoid breaking parameter names, etc. in table cells. */ +.doc-contents td code { + word-break: normal !important; +} + +/* No line break before first paragraph of descriptions. */ +.doc-md-description, +.doc-md-description>p:first-child { + display: inline; +} + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} + +.doc .md-typeset__table tr { + display: table-row; +} + +/* Defaults in Spacy table style. */ +.doc-param-default { + float: right; +} + +/* Symbols in Navigation and ToC. */ +:root, +[data-md-color-scheme="default"] { + --doc-symbol-attribute-fg-color: #953800; + --doc-symbol-function-fg-color: #8250df; + --doc-symbol-method-fg-color: #8250df; + --doc-symbol-class-fg-color: #0550ae; + --doc-symbol-module-fg-color: #5cad0f; + + --doc-symbol-attribute-bg-color: #9538001a; + --doc-symbol-function-bg-color: #8250df1a; + --doc-symbol-method-bg-color: #8250df1a; + --doc-symbol-class-bg-color: #0550ae1a; + --doc-symbol-module-bg-color: #5cad0f1a; +} + +[data-md-color-scheme="slate"] { + --doc-symbol-attribute-fg-color: #ffa657; + --doc-symbol-function-fg-color: #d2a8ff; + --doc-symbol-method-fg-color: #d2a8ff; + --doc-symbol-class-fg-color: #79c0ff; + --doc-symbol-module-fg-color: #baff79; + + --doc-symbol-attribute-bg-color: #ffa6571a; + --doc-symbol-function-bg-color: #d2a8ff1a; + --doc-symbol-method-bg-color: #d2a8ff1a; + --doc-symbol-class-bg-color: #79c0ff1a; + --doc-symbol-module-bg-color: #baff791a; +} + +code.doc-symbol { + border-radius: .1rem; + font-size: .85em; + padding: 0 .3em; + font-weight: bold; +} + +code.doc-symbol-attribute { + color: var(--doc-symbol-attribute-fg-color); + background-color: var(--doc-symbol-attribute-bg-color); +} + +code.doc-symbol-attribute::after { + content: "attr"; +} + +code.doc-symbol-function { + color: var(--doc-symbol-function-fg-color); + background-color: var(--doc-symbol-function-bg-color); +} + +code.doc-symbol-function::after { + content: "func"; +} + +code.doc-symbol-method { + color: var(--doc-symbol-method-fg-color); + background-color: var(--doc-symbol-method-bg-color); +} + +code.doc-symbol-method::after { + content: "meth"; +} + +code.doc-symbol-class { + color: var(--doc-symbol-class-fg-color); + background-color: var(--doc-symbol-class-bg-color); +} + +code.doc-symbol-class::after { + content: "class"; +} + +code.doc-symbol-module { + color: var(--doc-symbol-module-fg-color); + background-color: var(--doc-symbol-module-bg-color); +} + +code.doc-symbol-module::after { + content: "mod"; +} diff --git a/docs/gen_ref_nav.py b/docs/gen_ref_nav.py new file mode 100644 index 0000000..f509280 --- /dev/null +++ b/docs/gen_ref_nav.py @@ -0,0 +1,37 @@ +"""Generate the code reference pages and navigation.""" + +from pathlib import Path + +import mkdocs_gen_files + +SRC = Path("src") +PKG = SRC / "in_n_out" + +nav = mkdocs_gen_files.Nav() +mod_symbol = '' + +for path in sorted(SRC.rglob("*.py")): + module_path = path.relative_to("src").with_suffix("") + doc_path = path.relative_to(PKG).with_suffix(".md") + full_doc_path = Path("reference", doc_path) + + parts = tuple(module_path.parts) + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + if parts[-1].startswith("_"): + continue + + nav_parts = [f"{mod_symbol} {part}" for part in parts] + nav[tuple(nav_parts)] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + ident = ".".join(parts) + fd.write(f"::: {ident}") + + mkdocs_gen_files.set_edit_path(full_doc_path, ".." / path) + +with mkdocs_gen_files.open("reference/SUMMARY.txt", "w") as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..755f8bc --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,254 @@ +# Getting Started + +## Providers, Processors, and Stores + +`in-n-out` is a dependency injection framework for Python. It allows +you to write functions using type annotations, and then inject +dependencies into those functions at call time. Two important concepts +in `in-n-out` are *providers* and *processors*. + +- **Providers** are functions that may be called with no arguments + and return an instance of a type. +- **Processors** are functions that take an instance of a type and + do something with it. + +A [`Store`][in_n_out.Store] is a collection of providers and processors. +You will usually begin by creating a `Store` instance to manage your providers and +processors. + +```python +from in_n_out import Store + +store = Store.create('my-store') +``` + +This store can be retrieved later using `Store.get_store`, and +destroyed using `Store.destroy`. + +```python +store = Store.get_store('my-store') + +# Store.destroy('my-store') # would destroy the store... but we still want it :) +``` + +!!!info "The global store" + For convenience, any store methods accessed at the top level namespace will + use a global store, unless a store name or instance is passed + +## Registering Providers + +Dependency inject works by providing an instance of a type to a function +or method that requires it. Let's being by declaring some type that will be +important to our application: + +```python +class Thing: + """Some thing I care about.""" + def __init__(self, name: str): + self.name = name +``` + +Providers are registered using the [`Store.register_provider`][in_n_out.Store.register_provider]. They are functions that may be called with no arguments +and return an instance of a type. `in-n-out` will inspect the type annotations +of the function to determine what type it provides. + +```python +import in_n_out as ino + +def heres_the_thing() -> Thing: + return Thing("Thing") + +# register a provider of Thing +store.register_provider(heres_the_thing) +``` + +!!!tip "decorators" + Registration functions may also be used as decorators: + + ```python + @store.register_provider + def heres_the_thing() -> Thing: + return Thing("Thing") + ``` + +!!!note + If you prefer not to use type annotations, or prefer to be explicit about + the type the provider provides, you may pass a `type_hint` argument: + + ```python + store.register_provider(heres_the_thing, Thing) + ``` + +### Injecting dependencies into functions + +Once you have registered a provider, you can use it to inject dependencies +into functions. Let's say we have a function that can use a `Thing`: + +```python +def get_things_name(thing: Thing) -> str: + return thing.name +``` + +Naturally, this function will fail if we try to call it without providing +a `Thing`: + +```python +get_things_name() +# TypeError: get_things_name() missing 1 required positional argument: 'thing' +``` + +We can use the [`Store.inject`][in_n_out.Store.inject] method to inject a `Thing` into the function: + +```python +get_things_name = store.inject(get_things_name) +print(get_things_name()) # prints "Thing" +``` + +!!!tip "decorators" + As with registration functions, we can use `Store.inject` as a decorator: + + ```python + @store.inject + def get_things_name(thing: Thing) -> str: + return thing.name + + print(get_things_name()) # prints "Thing" + ``` + +If the store is unable to find a provider for a required type, it will raise +an exception: + +```python +@store.inject +def give_me_a_string(s: str) -> str: + return s + +give_me_a_string() +# TypeError: After injecting dependencies for NO arguments, +# give_me_a_string() missing 1 required positional argument: 's' +``` + +### Weights and provider priority + +You may register multiple providers for the same type. In this case, the +store will use the provider with the highest *weight*. + +```python +def give_me_another_thing() -> Thing: + return Thing("Another Thing") + +store.register_provider(give_me_another_thing, weight=10) +print(get_things_name()) # prints "Another Thing" +``` + +### Temporary registration + +Registration functions may be used as context managers to temporarily register +providers. + +```python +def most_important_thing() -> Thing: + return Thing("Most Important Thing") + +with store.register_provider(most_important_thing, weight=20): + print(get_things_name()) # prints "Most Important Thing" +print(get_things_name()) # prints "Another Thing" +``` + +### Undoing registration + +Alternatively, you can hang on to the object returned by the `register_provider` +function, and call its `cleanup` method: + +```python +token = store.register_provider(most_important_thing, weight=20) +print(get_things_name()) # prints "Most Important Thing" + +token.cleanup() # unregister +print(get_things_name()) # prints "Another Thing" +``` + +## Processors + +Processors are functions that take an instance of a type and do something +with it, usually for the purpose of side effects. Processors are registered +using the [`Store.register_processor`][in_n_out.Store.register_processor] method. + +```python +@store.inject_processors +def get_things_name(thing: Thing) -> str: + return thing.name + +def greet_name(name: str): + print(f"Hello, {name}!") + +store.register_processor(greet_name) + +get_things_name(Thing('Bob')) # prints "Hello, Bob!" (and still returns "Bob") +``` + +!!!warning "Careful" + Naturally, you want to be a bit careful with processors. It would be rather + unusual to register a processor for something as common as `str` as we + did above. Or, at the very least, we wouldn't inject processors into a + function that returned `str`. + +## Real world example + +Let's look at a more realistic example. + +Suppose we have an application like a text editor or IDE that allows plugins to +provide functionality like syntax highlighting, code completion, etc. We might +allow plugins to define functions that accept a `Document` and use it's API to +provide additional functionality. + +Rather than asking plugins to call some `get_current_document()` function, we +can allow them to write functions that state their dependencies using type hints, +and then *we* (the application) determine how we will provide those dependencies. + +```python title="some_plugin.py" +def highlight_document(document: Document): + # do something with the document + pass +``` + +```python title="my_application.py" +from in_n_out import Store + +# create a store +store = Store.create('my-store') + +# register a Document provider +@store.register_provider +def get_current_document() -> Document: + # get the current document from somewhere + ... + +# somehow gather functionality from plugins +from some_plugin import highlight_document + +plugin_functions = [highlight_document] + +# inject dependencies into plugin functions +injected = [store.inject(f) for f in plugin_functions] +``` + +Now we can call the injected functions, and they will have access +to the current document. + +### `app-model` + +In a GUI, one often needs to be able to call functions in response to +user actions, such as selection of a menu item or a button click. +However, those commands usually want some additional arguments or +context. Dependency injection provides a nice way to handle this +problem with loose coupling. + +See [`app-model`](https://github.com/pyapp-kit/app-model) for an +example of a library that uses `in-n-out` to inject dependencies +into commands in the context of a GUI application. + +## More information + +See the [API documentation](reference/index.md) for greater detail on the +`Store` class and its methods. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..a536a56 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,40 @@ +# in-n-out + +Python dependency injection you can taste. :fontawesome-solid-burger: + +A lightweight dependency injection and result processing framework +for Python using type hints. Emphasis is on simplicity, ease of use, +and minimal impact on source code. + +```python +import in_n_out as ino + +# register functions that provide a dependency +@ino.register_provider +def some_number() -> int: + return 42 + +# inject dependencies into functions +@ino.inject +def print_square(num: int): + print(num ** 2) + +print_square() # prints 1764 +``` + +See the [Getting Started](getting_started.md) guide for a quick introduction or +the [API Reference](reference/index.md) for detailed documentation. + +## Installation + +Install from pip + +```bash +pip install in-n-out +``` + +Or from conda-forge + +```bash +conda install -c conda-forge in-n-out +``` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c835459 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,77 @@ +site_name: in-n-out +site_url: https://github.com/pyapp-kit/in-n-out +site_author: Talley Lambert +site_description: Python dependency injection you can taste +# strict: true + +repo_name: pyapp-kit/in-n-out +repo_url: https://github.com/pyapp-kit/in-n-out + +copyright: Copyright © 2021 - 2023 Talley Lambert + +watch: + - src + +nav: + - index.md + - getting_started.md + # defer to gen-files + literate-nav + - API reference: reference/ + +plugins: + - search + - gen-files: + scripts: + - docs/gen_ref_nav.py + - literate-nav: + nav_file: SUMMARY.txt + - autorefs + - mkdocstrings: + handlers: + python: + import: + - https://docs.python.org/3/objects.inv + options: + docstring_style: numpy + docstring_options: + ignore_init_summary: true + docstring_section_style: list + filters: ["!^_"] + heading_level: 1 + inherited_members: true + merge_init_into_class: true + separate_signature: true + show_root_heading: true + show_root_full_path: false + show_signature_annotations: true + show_bases: true + show_source: true + +markdown_extensions: + - tables + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - pymdownx.details + - admonition + - toc: + permalink: "#" + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +theme: + name: material + icon: + repo: material/github + logo: fontawesome/solid/syringe + features: + - navigation.instant + - navigation.indexes + - search.highlight + - search.suggest + - navigation.expand + - content.code.copy + +extra_css: + - css/style.css diff --git a/pyproject.toml b/pyproject.toml index 7a569a7..2acd6a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,11 +52,18 @@ dev = [ "pytest", "rich", ] +docs = [ + "mkdocs-gen-files", + "mkdocs-literate-nav", + "mkdocs-material==9.4.1", + "mkdocs==1.5.3", + "mkdocstrings-python==1.7.3", +] [project.urls] homepage = "https://github.com/pyapp-kit/in-n-out" repository = "https://github.com/pyapp-kit/in-n-out" -documentation = "https://pyapp-kit.github.io/in-n-out" +documentations = "https://ino.rtfd.io" # https://github.com/charliermarsh/ruff [tool.ruff] @@ -129,15 +136,16 @@ disallow_untyped_defs = false # https://github.com/mgedmin/check-manifest#configuration [tool.check-manifest] ignore = [ - ".cruft.json", - ".flake8", ".github_changelog_generator", ".pre-commit-config.yaml", "tests/**/*", + "docs/**/*", "**/*.c", "Makefile", "codecov.yml", + "mkdocs.yml", "asv.conf.json", "benchmarks/**/*", "CHANGELOG.md", + ".readthedocs.yaml", ] diff --git a/src/in_n_out/__init__.py b/src/in_n_out/__init__.py index fa1c466..e6e6afa 100644 --- a/src/in_n_out/__init__.py +++ b/src/in_n_out/__init__.py @@ -1,4 +1,13 @@ -"""plugable dependency injection and result processing.""" +"""plugable dependency injection and result processing. + +Generally speaking, providers and processors are defined as follows: + +- `Provider: TypeAlias = Callable[[], Any]`: a callable that can accept no arguments and + returns an instance of some type. When we refer to a +- `Processor: TypeAlias = Callable[[Any], Any]`: a callable that accepts a single + positional argument (an instance of some type) and returns anything (the return + value is ignored). +""" from importlib.metadata import PackageNotFoundError, version @@ -31,17 +40,17 @@ from ._util import _compiled __all__ = [ - "register_provider", "_compiled", + "inject_processors", "inject", "iter_processors", "iter_providers", - "inject_processors", - "process", "mark_processor", - "provide", "mark_provider", + "process", + "provide", "register_processor", + "register_provider", "register", "register", "resolve_single_type_hints", diff --git a/src/in_n_out/_global.py b/src/in_n_out/_global.py index 2529fb1..de5766d 100644 --- a/src/in_n_out/_global.py +++ b/src/in_n_out/_global.py @@ -53,6 +53,10 @@ def register( providers: ProviderIterable | None = None, store: str | Store | None = None, ) -> InjectionContext: + """Register multiple providers and/or processors in `store` or the global store. + + See [`Store.register`][in_n_out.Store.register] for details. + """ return _store_or_global(store).register(providers=providers, processors=processors) @@ -63,6 +67,10 @@ def register_provider( weight: float = 0, store: str | Store | None = None, ) -> InjectionContext: + """Register a provider in `store` or the global store. + + See [`Store.register_provider`][in_n_out.Store.register_provider] for details. + """ return _store_or_global(store).register_provider( provider=provider, type_hint=type_hint, weight=weight ) @@ -75,6 +83,10 @@ def register_processor( weight: float = 0, store: str | Store | None = None, ) -> InjectionContext: + """Register a processor in `store` or the global store. + + See [`Store.register_processor`][in_n_out.Store.register_processor] for details. + """ return _store_or_global(store).register_processor( processor=processor, type_hint=type_hint, weight=weight ) @@ -110,6 +122,10 @@ def mark_provider( type_hint: object | None = None, store: str | Store | None = None, ) -> Callable[[ProviderVar], ProviderVar] | ProviderVar: + """Decorate `func` as a provider in `store` or the global store. + + See [`Store.mark_provider`][in_n_out.Store.mark_provider] for details. + """ return _store_or_global(store).mark_provider( func, weight=weight, type_hint=type_hint ) @@ -145,6 +161,10 @@ def mark_processor( type_hint: object | None = None, store: str | Store | None = None, ) -> Callable[[ProcessorVar], ProcessorVar] | ProcessorVar: + """Decorate `func` as a processor in `store` or the global store. + + See [`Store.mark_processor`][in_n_out.Store.mark_processor] for details. + """ return _store_or_global(store).mark_processor( func, weight=weight, type_hint=type_hint ) @@ -154,6 +174,10 @@ def mark_processor( def iter_providers( type_hint: object | type[T], store: str | Store | None = None ) -> Iterable[Callable[[], T | None]]: + """Iterate over all providers of `type_hint` in `store` or the global store. + + See [`Store.iter_providers`][in_n_out.Store.iter_providers] for details. + """ return _store_or_global(store).iter_providers(type_hint) @@ -161,6 +185,10 @@ def iter_providers( def iter_processors( type_hint: object | type[T], store: str | Store | None = None ) -> Iterable[Callable[[T], Any]]: + """Iterate over all processors of `type_hint` in `store` or the global store. + + See [`Store.iter_processors`][in_n_out.Store.iter_processors] for details. + """ return _store_or_global(store).iter_processors(type_hint) @@ -169,6 +197,10 @@ def provide( type_hint: object | type[T], store: str | Store | None = None, ) -> T | None: + """Provide an instance of `type_hint` with providers from `store` or the global store. + + See [`Store.provide`][in_n_out.Store.provide] for details. + """ # noqa: E501 return _store_or_global(store).provide(type_hint=type_hint) @@ -181,6 +213,10 @@ def process( raise_exception: bool = False, store: str | Store | None = None, ) -> None: + """Process an instance of `type_` with processors from `store` or the global store. + + See [`Store.process`][in_n_out.Store.process] for details. + """ return _store_or_global(store).process( result=result, type_hint=type_hint, @@ -234,6 +270,10 @@ def inject( guess_self: bool | None = None, store: str | Store | None = None, ) -> Callable[..., R] | Callable[[Callable[P, R]], Callable[..., R]]: + """Decorate `func` to inject dependencies at calltime from `store` or the global store. + + See [`Store.inject`][in_n_out.Store.inject] for details. + """ # noqa: E501 return _store_or_global(store).inject( func=func, providers=providers, @@ -278,6 +318,10 @@ def inject_processors( raise_exception: bool = False, store: str | Store | None = None, ) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]: + """Decorate a function to process its output from `store` or the global store. + + See [`Store.inject_processors`][in_n_out.Store.inject_processors] for details. + """ return _store_or_global(store).inject_processors( func=func, type_hint=hint, diff --git a/src/in_n_out/_store.py b/src/in_n_out/_store.py index 6d8c4a6..4060427 100644 --- a/src/in_n_out/_store.py +++ b/src/in_n_out/_store.py @@ -263,9 +263,9 @@ def register( Parameters ---------- - providers :Optional[CallbackIterable] + providers : CallbackIterable | None mapping or iterable of providers to register. See format in notes above. - processors :Optional[CallbackIterable] + processors : CallbackIterable | None mapping or iterable of processors to register. See format in notes above. Returns @@ -302,7 +302,7 @@ def register_provider( ---------- provider : Callable A provider callback. Must be able to accept no arguments. - type_hint : Optional[object] + type_hint : object | None A type or type hint that `provider` provides. If not provided, it will be inferred from the return annotation of `provider`. weight : float, optional @@ -382,9 +382,9 @@ def mark_provider( Parameters ---------- - func : Optional[Provider] + func : Provider | None A function to decorate. If not provided, a decorator is returned. - type_hint : Optional[object] + type_hint : object | None Optional type or type hint for which to register this provider. If not provided, the return annotation of `func` will be used. weight : float @@ -393,7 +393,7 @@ def mark_provider( Returns ------- - Union[Callable[[Provider], Provider], Provider] + Callable[[Provider], Provider] | Provider If `func` is not provided, a decorator is returned, if `func` is provided then the function is returned.. @@ -444,9 +444,9 @@ def mark_processor( Parameters ---------- - func : Optional[Processor], optional + func : Processor | None A function to decorate. If not provided, a decorator is returned. - type_hint : Optional[object] + type_hint : object | None Optional type or type hint that this processor can handle. If not provided, the type hint of the first parameter of `func` will be used. weight : float, optional @@ -457,7 +457,7 @@ def mark_processor( Returns ------- - Union[Callable[[Processor], Processor], Processor] + Callable[[Processor], Processor] | Processor If `func` is not provided, a decorator is returned, if `func` is provided then the function is returned. @@ -486,12 +486,12 @@ def iter_providers( Parameters ---------- - type_hint : Union[object, Type[T]] + type_hint : object | Type[T] A type or type hint for which to return providers. Yields ------ - Iterable[Callable[[], Optional[T]]] + Iterable[Callable[[], T | None]] Iterable of provider callbacks. """ return self._iter_type_map(type_hint, self._cached_provider_map) @@ -503,12 +503,12 @@ def iter_processors( Parameters ---------- - type_hint : Union[object, Type[T]] + type_hint : object | Type[T] A type or type hint for which to return processors. Yields ------ - Iterable[Callable[[], Optional[T]]] + Iterable[Callable[[], T | None]] Iterable of processor callbacks. """ return self._iter_type_map(type_hint, self._cached_processor_map) @@ -523,12 +523,12 @@ def provide(self, type_hint: object | type[T]) -> T | None: Parameters ---------- - type_hint : Union[object, Type[T]] + type_hint : object | Type[T] A type or type hint for which to return a value Returns ------- - Optional[T] + T | None The first non-`None` value returned by a provider, or `None` if no providers return a value. """ @@ -557,7 +557,7 @@ def process( ---------- result : Any The result to process - type_hint : Union[object, Type[T], None], + type_hint : object | type[T] | None An optional type hint to provide to the processor. If not provided, the type of `result` will be used. first_processor_only : bool, optional @@ -660,41 +660,39 @@ def inject( this function is called. Important: this causes *side effects*. By default, `False`. Output processing can also be enabled (with additionl fine tuning) by using the `@store.process_result` decorator. - localns : Optional[dict] + localns : dict | None Optional local namespace for name resolution, by default None on_unresolved_required_args : RaiseWarnReturnIgnore What to do when a required parameter (one without a default) is encountered with an unresolvable type annotation. Must be one of the following (by default 'warn'): - - 'raise': immediately raise an exception - - 'warn': warn and return the original function - - 'return': return the original function without warning - - 'ignore': continue decorating without warning (at call time, this - function will fail without additional arguments). - + - `'raise'`: immediately raise an exception + - `'warn'`: warn and return the original function + - `'return'`: return the original function without warning + - `'ignore'`: continue decorating without warning (at call time, this + function will fail without additional arguments). on_unannotated_required_args : RaiseWarnReturnIgnore What to do when a required parameter (one without a default) is encountered with an *no* type annotation. These functions are likely to fail when called later if the required parameter is not provided. Must be one of the following (by default 'warn'): - - 'raise': immediately raise an exception - - 'warn': warn, but continue decorating - - 'return': immediately return the original function without warning - - 'ignore': continue decorating without warning. - + - `'raise'`: immediately raise an exception + - `'warn'`: warn, but continue decorating + - `'return'`: immediately return the original function without warning + - `'ignore'`: continue decorating without warning. guess_self : bool Whether to infer the type of the first argument if the function is an unbound class method (by default, `True`) This is done as follows: - - if '.' (but not '') is in the function's __qualname__ - - and if the first parameter is named 'self' or starts with "_" - - and if the first parameter annotation is `inspect.empty` - - then the name preceding `func.__name__` in the function's __qualname__ - (which is usually the class name), is looked up in the function's - `__globals__` namespace. If found, it is used as the first parameter's - type annotation. + - if `'.'` (but not `''`) is in the function's `__qualname__` + - and if the first parameter is named 'self' or starts with `"_"` + - and if the first parameter annotation is `inspect.empty` + - then the name preceding `func.__name__` in the function's `__qualname__` + (which is usually the class name), is looked up in the function's + `__globals__` namespace. If found, it is used as the first parameter's + type annotation. This allows class methods to be injected with instances of the class. @@ -889,8 +887,8 @@ def inject_processors( ) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]: """Decorate a function to process its output. - Variant of inject, but only injects processors (for the sake of more explicit - syntax). + Variant of [`inject`][in_n_out.Store.inject], but only injects processors + (for the sake of more explicit syntax). When the decorated function is called, the return value will be processed with `store.process(return_value)` before returning the result. @@ -901,7 +899,7 @@ def inject_processors( ---------- func : Callable A function to decorate. Return hints are used to determine what to process. - type_hint : Union[object, Type[T], None] + type_hint : object | type[T] | None Type hint for the return value. If not provided, the type will be inferred first from the return annotation of the function, and if that is not provided, from the `type(return_value)`. diff --git a/src/in_n_out/_type_resolution.py b/src/in_n_out/_type_resolution.py index 5835d55..fcd99e8 100644 --- a/src/in_n_out/_type_resolution.py +++ b/src/in_n_out/_type_resolution.py @@ -45,15 +45,15 @@ def resolve_type_hints( This is a small wrapper around `typing.get_type_hints()` that adds namespaces to the global and local namespaces. - see docstring for :func:`typing.get_type_hints`. + see docstring for [`typing.get_type_hints`][typing.get_type_hints]. Parameters ---------- obj : module, class, method, or function must be a module, class, method, or function. - globalns : Optional[dict] + globalns : dict | None optional global namespace, by default None. - localns : Optional[dict] + localns : dict | None optional local namespace, by default None. include_extras : bool If `False` (the default), recursively replaces all 'Annotated[T, ...]' @@ -61,7 +61,7 @@ def resolve_type_hints( Returns ------- - Dict[str, Any] + dict[str, Any] mapping of object name to type hint for all annotated attributes of `obj`. """ _localns = dict(_typing_names()) @@ -81,20 +81,23 @@ def resolve_single_type_hints( ) -> tuple[Any, ...]: """Get type hints for one or more isolated type annotations. - Wrapper around :func:`resolve_type_hints` (see docstring for that function for - parameter docs). + Wrapper around [`resolve_type_hints`][in_n_out.resolve_type_hints] + (see docstring for that function for parameter docs). - `typing.get_type_hints()` only works for modules, classes, methods, or functions, - but the typing module doesn't make the underlying type evaluation logic publicly - available. This function creates a small mock object with an `__annotations__` - dict that will work as an argument to `typing.get_type_hints()`. It then extracts - the resolved hints back into a tuple of hints corresponding to the input objects. + [`typing.get_type_hints`][typing.get_type_hints] only works for modules, classes, + methods, or functions, but the typing module doesn't make the underlying type + evaluation logic publicly available. This function creates a small mock object with + an `__annotations__` dict that will work as an argument to + `typing.get_type_hints()`. It then extracts the resolved hints back into a tuple of + hints corresponding to the input objects. Returns ------- - Tuple[Any, ...] + tuple[Any, ...] Tuple + Examples + -------- >>> resolve_single_type_hints('hi', localns={'hi': typing.Any}) (typing.Any,) """ @@ -120,7 +123,7 @@ def type_resolved_signature( ---------- func : Callable A callable object. - localns : Optional[dict] + localns : dict | None Optional local namespace for name resolution, by default None raise_unresolved_optional_args : bool Whether to raise an exception when an optional parameter (one with a default @@ -143,7 +146,8 @@ class method. This is done as follows: Returns ------- Signature - :class:`inspect.Signature` object with fully resolved type annotations, + [`inspect.Signature`][inspect.Signature] object with fully resolved type + annotations, (or at least partially resolved type annotations if `raise_unresolved_optional_args` is `False`). @@ -219,10 +223,11 @@ def _resolve_params_one_by_one( Parameters ---------- sig : Signature - :class:`inspect.Signature` object with unresolved type annotations. - globalns : Optional[dict] + [`inspect.Signature`][inspect.Signature] object with unresolved type + annotations. + globalns : dict | None Optional global namespace for name resolution, by default None - localns : Optional[dict] + localns : dict | None Optional local namespace for name resolution, by default None exclude_unresolved_optionals : bool Whether to exclude parameters with unresolved type annotations that have a @@ -233,7 +238,7 @@ def _resolve_params_one_by_one( Returns ------- - Dict[str, Any] + dict[str, Any] mapping of parameter name to type hint. Raises