Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature request] Automagical Import Hook #43

Closed
leycec opened this issue Jul 29, 2021 · 40 comments
Closed

[Feature request] Automagical Import Hook #43

leycec opened this issue Jul 29, 2021 · 40 comments

Comments

@leycec
Copy link
Member

leycec commented Jul 29, 2021

@zeevox of SentiPy and Sentiment Investor fame kindly requested automation to automagically decorate all callables in a given package with @beartype without needing to explicitly do so:

Is it necessary to annotate each function with @beartype? Is there a way of more concisely adding beartype everywhere automagically?

This feature request tracks our eventual (hopefully not too eventual) solution: a beartype import hook. Specifically:

  • We need to implement a new AST-based beartype.hook.beartype_everything() import hook inspired by typeguard's comparable typeguard.importhook.install_import_hook() function. That's only 162 lines of pure Python, which means this is mostly trivial, but it's a dense 162 lines, which means this might take an undefinable quantum of space-time.
  • Interested third parties will then call our import hook at the top of their top-level __init__ submodules to globally and transparently apply @beartype across their entire codebases.

Grab that popcorn! @leycec's Wild Ride is now boarding for an improved @beartype user experience (UX).

@Skylion007
Copy link

Having this usable as a PyTest extension to decorate all PyTest testsuite calls like TypeGuard does would be automagical. One of our projects used to use Typeguard's PyTest setup for the CI, but we had to drop it because it was too slow with our PyTorch tests.

@leycec
Copy link
Member Author

leycec commented Aug 1, 2021

Oh. Oh, yes. Thanks for the instructive heads up on typeguard's --typeguard-packages pytest option, flying lion with a secretive British license to kill. Allow me to publicly applaud your GitHub avatar as well, which may very well be the cutest thing I have ever seen – and I watch anime. Voluntarily.

I did notice typeguard surprisingly installed a pytest extension when I packaged typeguard for Gentoo Linux a year ago, but never actually found the scarce time to dig into what exactly that extension was doing. Now I know and feel modestly smarter for it.

A beartype pytest extension is an even higher-level abstraction and more automagical than an import hook. Great! This will automate the import hook that automates the decoration that automates the type-checking. The automation is gettin' crazy.

I've added yet another feature request tracking a pytest extension so I feel slightly less overwhelmed. It probably won't help. 😅

leycec added a commit that referenced this issue May 10, 2022
This commit is the first in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit crudely outlines the
new `beartype.claw` subpackage housing this API and a new private
`beartype.claw._clawcore` submodule of that subpackage implementing this
API. Naturally, nothing works.

Unrelatedly, this commit also disables previously enabled testing of
pre-release builds of Python 3.11 under our GitHub Actions-based
continuous integration (CI) workflow. Why? Because doing so foolishly
attempts to compile NumPy from source, which unsurprisingly fails with
catastrophic fireworks.

(*Inimitable indomitable table!*)
leycec added a commit that referenced this issue May 11, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit internally describes a
possible avenue forward resolving all outstanding issues with import
hook-based abstract syntax tree (AST) transformations performed by other
competing packages (e.g., `typeguard`, `ideas`).

Unrelatedly, this commit yet again disables previously enabled testing
of pre-release builds of Python 3.11 under our GitHub Actions-based
continuous integration (CI) workflow. Why? Because doing so foolishly
attempts to compile NumPy from source, which unsurprisingly fails with
catastrophic fireworks.

(*Descriptively proactive proscription!*)
leycec added a commit that referenced this issue May 12, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit defines a new private
PEP 302- and 451-compliant
`beartype.claw._clawcore._BeartypeSourceFileLoader` class compatible
with standard `importlib` machinery for specifying import path hooks.
Naturally, nothing works. (*Tacit tactics!*)
leycec added a commit that referenced this issue May 13, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit:

* Defines a new private `beartype.claw._clawast` submodule intended to
  house our our as-yet-undefined abstract syntax tree (AST)
  transformation.
* Renames `beartype.claw._clawcore` to `beartype.claw._clawimportlib`
  for disambiguity.
* Begins implementing our previously defined
  `beartype.claw._clawimportlib._BeartypeSourceFileLoader` class.
  Notably, the `source_to_code()` method now sports a reasonable
  facsimile of a working implementation invoking our as-yet-undefined
  abstract syntax tree (AST) transformation.

(*Discretionary n-ary diversions!*)
leycec added a commit that referenced this issue May 17, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit:

* Renames the existing private `beartype._util.func.utilfuncstack`
  submodule to `utilfuncframe` as well as all functions declared by that
  submodule for clarity and readability.
* Documents a medley of proposed improvements to the existing private
  non-working `beartype.claw._clawimportlib` submodule, particularly
  with respect to rendering the currently mandatory `package_names`
  parameter accepted by the public
  `beartype_package_submodules_when_imported()` function optional.

(*Acyclicly tricky tricycles!*)
leycec added a commit that referenced this issue May 18, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit:

* Renames a sweeping number of unrelated private submodules for
  disambiguity, clarity, and usability.
* Extensively documents the private
  `beartype.claw._clawimportlib._package_basename_to_subpackages`
  global internally backing this API.
* Renders the ``package_names`` parameter accepted by this API optional
  through the magic of magical call stack inspection.
* Validates the same parameter with extensive manual type-checking.
* Documents further prospective changes needed to fully realize this
  tumescent dream of automated ``O(1)`` runtime type-checking.

(*Effervescent scent of verdant fervency!*)
leycec added a commit that referenced this issue May 19, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit:

* Isolates all private functionality pertaining to package name caching
  to a new `beartype.claw._clawpackagenames` submodule.
* In this submodule:
  * Defines a new `register_package_names()` function caching the passed
    package names and associated beartype configuration for subsequent
    thread-safe lookup by beartype import path hooks.
  * Defines a new `_PackageBasenameToSubpackagesDict` subclass embodying
    each item of the recursive nested dictionary structure caching these
    package names and associated beartype configuration.

(*Wayward awkward skyward ward city!*)
leycec added a commit that referenced this issue May 20, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit finalizes a draft
implementation of our newly implemented
`beartype.claw._clawpackagenames.register_package_names()` function.
(*Humbly bumbling bumblebee!*)
leycec added a commit that referenced this issue May 21, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit:

* Finalizes a draft implementation of the public
  `beartype.claw._clawimportlib.beartype_submodules_on_import()`
  function, which now (purports to) install a beartype import path hook
  implicitly decorating all callables defined by all submodules defined
  by all packages with the passed names in an optimally efficient
  manner. And... we're spent.
* Monkey-patches the private
  `beartype.claw._clawimportlib._BeartypeSourceFileLoader.get_code()`
  method to apply a beartype-specific optimization marker.

(*Undecidable quandaries of unquantifiable friable friars without boundaries!*)
leycec added a commit that referenced this issue May 23, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit extensively documents
the mostly implemented private
`beartype.claw._clawimportlib._BeartypeSourceFileLoader` class core to
our import hook implementation. (*Derogatory interrogatory purgatory!*)
leycec added a commit that referenced this issue May 25, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit:

* Defines a new public `beartype.claw.beartype_all()` hook intended to
  be called only from proprietary application stacks.
* Splits our prior private `beartype.claw._clawimportlib` submodule into
  two private `beartype.claw._claw{loader,pathhooks}` submodules.

(*Eminent immanence feeds impertinent impermanence!*)
leycec added a commit that referenced this issue May 25, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit (yet again) mostly
just documents a path forward through the dark machinations of the
`importlib` module's machinery. (*Abiotic allometric allergen!*)
leycec added a commit that referenced this issue May 26, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit defines a new private
`beartype.claw._clawpackagenames.is_package_registered()` tester
enabling our higher-level private
`beartype.claw._clawloader.BeartypeSourceFileLoader` class to decide
whether the module currently being loaded should be subject to implicit
`@beartype` decoration or not. (*Saucy innocuous insouciance!*)
leycec added a commit that referenced this issue May 28, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit:

* Renames a number of private submodules for disambiguity.
* Finalizes our draft implementations of the private
  `beartype.claw._clawloader` and `beartype.claw._clawregistrar`
  submodules.
* Defines a new private `beartype._decor.decorcore.beartype_object_safe`
  decorator reducing all fatal exceptions raised by the `@beartype`
  decorator to non-fatal warnings, intended to be applied *only* when
  recursively applying `@beartype` to an entire package at importation
  time. Doing so should substantially reduce the fragility of our import
  hook API... in theory.

(*Coddled codpiece? Codswallop!*)
leycec added a commit that referenced this issue May 28, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit finalizes our draft
implementations of the private `beartype.claw._clawast` submodule. The
only remaining portion of this API to be implemented is the related
private `@beartype._decor.decorcore.beartype_object_safe` decorator;
while technically implemented, output emitted by that decorator is
currently unsuitable for use in large codebases. Nearly there! (*Dearly hair!*)
leycec added a commit that referenced this issue May 31, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit:

* Finalizes our draft implementation of the public
  `beartype.claw.beartype_all()` function.
* Continues implementing our private
  `@beartype._decor.decorcore.beartype_object_safe` decorator, which
  remains woefully inadequate against real-world errors.

(*Jocular gesticulation!*)
leycec added a commit that referenced this issue Jun 1, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit defines a new private
`beartype._util.mod.utilmodget.get_object_module_line_number_begin()`
retrieving the line number at which the passed callable or class is
defined in its parent module, which our private
`@beartype._decor.decorcore.beartype_object_safe` decorator will
subsequently embed in warning messages. (*Plangent plan, gentlemen!*)
leycec added a commit that referenced this issue Jun 2, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit generalizes our
existing private `beartype._util.text.utiltextlabel.label_callable()`
function with an optional `is_contextualized` parameter -- which, when
enabled, contextualizes the returned label with the filename and line
number declaring the passed callable. (*Tentative representative!*)
leycec added a commit that referenced this issue Jun 5, 2022
This commit is the next in a commit chain defining our new
**all-at-once API** (i.e., an import hook enabling external callers to
trivially decorate well-typed third-party packages and modules with
runtime type-checking dynamically generated by the
`beartype.beartype` decorator in a single line of code), en-route to
resolving issue #43 kindly submitted by sentimental investor
extraordinaire @zeevox. Specifically, this commit improves unit tests
exercising our `beartype._util.text.utiltextlabel.label_callable()`
function with improved coverage over noteworthy edge cases – including
coroutine, asynchronous generator, and synchronous generator factory
callables. (*Fractuous fractalizations distil the tillerman's generalizations!*)
@The-Compiler
Copy link

So, I'm not sure if this is a bug, or if I'm just doing something wrong - I haven't used beartype before (or I did, and forgot?), and thought I'd give it a try on qutebrowser.

For a full reproducer:

$ git clone https://github.com/qutebrowser/qutebrowser
$ git checkout 8da62bcbf4e90cc3952decf72b6798540f4b9d10  # current master branch at the time of writing
$ cd qutebrowser
$ python3 -m venv .venv
$ .venv/bin/pip install -r requirements.txt PyQt5 PyQtWebEngine git+https://github.com/beartype/beartype.git@34931c1a58157daaa0518f9094e702209af86661
$ echo "import beartype.claw; beartype.claw.beartype_this_package()" >> qutebrowser/__init__.py
$ .venv/bin/python -m qutebrowser --temp-basedir

results in:

/home/florian/tmp/qutebrowser/.venv/lib/python3.11/site-packages/beartype/_util/hint/pep/utilpeptest.py:311: BeartypeDecorHintPep585DeprecationWarning: PEP 484 type hint typing.Dict[str, str] deprecated by PEP 585. This hint is scheduled for removal in the first Python version released after October 5th, 2025. To resolve this, import this hint from "beartype.typing" rather than "typing". For further commentary and alternatives, see also:
    https://beartype.readthedocs.io/en/latest/api_roar/#pep-585-deprecations
  warn(
Traceback (most recent call last):
  File "/home/florian/tmp/qutebrowser/.venv/lib/python3.11/site-packages/beartype/door/_doorcheck.py", line 147, in die_if_unbearable
    _check_object(obj)
  File "<@beartype(beartype.door._doorcheck._get_type_checker._die_if_unbearable) at 0x7efc2e9ad940>", line 18, in _die_if_unbearable
beartype.roar.BeartypeCallHintReturnViolation: Function beartype.door._doorcheck._get_type_checker._die_if_unbearable() return "Field(name=None,type=None,default=<dataclasses._MISSING_TYPE object at 0x7efc2ea7f390>,defa..._ violates type hint typing.Dict[str, str] under non-default configuration BeartypeConf(claw_is_pep526=True, is_color=None, is_debug=False, is_pep484_tower=False, reduce_decorator_exception_to_warning_category=<class 'beartype.roar._roarwarn.BeartypeClawDecorWarning'>, strategy=<BeartypeStrategy.O1: 2>), as <class "dataclasses.Field"> "Field(name=None,type=None,default=<dataclasses._MISSING_TYPE object at 0x7efc2ea7f390>,defa..._ not instance of dict.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/florian/tmp/qutebrowser/qutebrowser/__main__.py", line 23, in <module>
    import qutebrowser.qutebrowser
  File "/home/florian/tmp/qutebrowser/qutebrowser/qutebrowser.py", line 54, in <module>
    from qutebrowser.misc import earlyinit
  File "/home/florian/tmp/qutebrowser/qutebrowser/misc/earlyinit.py", line 47, in <module>
    from qutebrowser.qt import machinery
  File "/home/florian/tmp/qutebrowser/qutebrowser/qt/machinery.py", line 86, in <module>
    class SelectionInfo:
  File "/home/florian/tmp/qutebrowser/qutebrowser/qt/machinery.py", line 90, in SelectionInfo
    outcomes: Dict[str, str] = dataclasses.field(default_factory=dict)
  File "/home/florian/tmp/qutebrowser/.venv/lib/python3.11/site-packages/beartype/door/_doorcheck.py", line 177, in die_if_unbearable
    raise BeartypeDoorHintViolation(
beartype.roar.BeartypeDoorHintViolation: Object "Field(name=None,type=None,default=<dataclasses._MISSING_TYPE object at 0x7efc2ea7f390>,defa..._ violates type hint typing.Dict[str, str] under non-default configuration BeartypeConf(claw_is_pep526=True, is_color=None, is_debug=False, is_pep484_tower=False, reduce_decorator_exception_to_warning_category=<class 'beartype.roar._roarwarn.BeartypeClawDecorWarning'>, strategy=<BeartypeStrategy.O1: 2>), as <class "dataclasses.Field"> "Field(name=None,type=None,default=<dataclasses._MISSING_TYPE object at 0x7efc2ea7f390>,defa..._ not instance of dict.

which got me stumped. For one, File "<@beartype(beartype.door._doorcheck._get_type_checker._die_if_unbearable) at 0x7efc2e9ad940>" in the first selection sounds like a weird filename! 🙃.

But also, it points here:

https://github.com/qutebrowser/qutebrowser/blob/8da62bcbf4e90cc3952decf72b6798540f4b9d10/qutebrowser/qt/machinery.py#L90

which seems to be correct...

I tried extracting things into a more minimal example, by creating a pkg folder, with an __init__.py:

import beartype.claw
beartype.claw.beartype_all()

and an x.py:

import dataclasses
import enum
from typing import Dict, Optional

class SelectionReason(enum.Enum):

    """Reasons for selecting a Qt wrapper."""

    #: The wrapper was selected via --qt-wrapper.
    cli = "--qt-wrapper"

    #: The wrapper was selected via the QUTE_QT_WRAPPER environment variable.
    env = "QUTE_QT_WRAPPER"

    #: The wrapper was selected via autoselection.
    auto = "autoselect"

    #: The default wrapper was selected.
    default = "default"

    #: The wrapper was faked/patched out (e.g. in tests).
    fake = "fake"

    #: The reason was not set.
    unknown = "unknown"


@dataclasses.dataclass
class SelectionInfo:
    """Information about outcomes of importing Qt wrappers."""

    wrapper: Optional[str] = None
    outcomes: Dict[str, str] = dataclasses.field(default_factory=dict)
    reason: SelectionReason = SelectionReason.unknown


s = SelectionInfo()

and then running python -m pkg.x, but that greets me with:

Traceback (most recent call last):
  File "/home/florian/tmp/.venv/lib/python3.11/site-packages/beartype/claw/_importlib/clawimpcache.py", line 101, in __getitem__
    return super().__getitem__(module_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: '__main__'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/florian/tmp/pkg/x.py", line 5, in <module>
    class SelectionReason(enum.Enum):
  File "/home/florian/tmp/.venv/lib/python3.11/site-packages/beartype/claw/_importlib/clawimpcache.py", line 105, in __getitem__
    raise BeartypeClawImportConfException(
beartype.roar.BeartypeClawImportConfException: Beartype configuration associated with module "__main__" hooked by "beartype.claw" import hooks not found. Existing beartype configurations associated with such modules include:
{'pkg.x': BeartypeConf(claw_is_pep526=True, is_color=None, is_debug=False, is_pep484_tower=False, reduce_decorator_exception_to_warning_category=<class 'beartype.roar._roarwarn.BeartypeClawDecorWarning'>, strategy=<BeartypeStrategy.O1: 2>)}

@gabrieldemarmiesse
Copy link

gabrieldemarmiesse commented Jul 9, 2023

You are getting the same issue I had with pydantic, but pydantic uses Annotated to fix it, while dataclass does not.
Reading the source code of field in dataclasses, I can see the comment:

# This function is used instead of exposing Field creation directly,
# so that a type checker can be told (via overloads) that this is a
# function whose type depends on its parameters.

It's all fine and dandy but it's just misleading the static type checker. The type returned is always, always dataclasses.Field. As such, typebear is seeing through the trick of dataclasses.field since it sees the true types at runtime.

Since dataclasses doesn't provide the annotated option, I think the only way to fix the dataclass type mistake in the stdlib and declare the real types is this:

from dataclasses import dataclass, field, Field

@dataclass
class MyClass:
    my_list: list[str] | Field = field(default_factory=list)

@leycec this could also go in the bearpedia I believe

@leycec
Copy link
Member Author

leycec commented Jul 9, 2023

OMG. The beartype.claw tech preview erupts in chaos. There's a medley of intersecting issues here – most of which are @beartype's fault but only a few of which are beartype.claw's fault:

  • Pydantic integration. I confess I've never tried integrating @beartype + Pydantic. Unsurprisingly, they hate each other. Pydantic shamefully lies gently massages type hints into Pydantic-specific objects, which is understandable from its perspective but less understandable from our perspective. Although the typing.Annotated workaround is certainly admirable, @beartype should really just silently look the other way when presented with Pydantic types. I've opened a new feature request tracking this at #248.
  • Dataclass integration. I thought we'd nailed to the floor our @beartype + @dataclass integration. I thought wrong. This is probably a beartype.claw-specific issue. Let's see what I can jury-rig together in my copious lack of free time. Gah!

And so much more unexpected madness. I'm eternally grateful, everyone! I can confidently say that @beartype 0.15.0 is now due by September 2057 at the latest. My forehead is crinkling. 😦

@leycec
Copy link
Member Author

leycec commented Jul 10, 2023

@The-Compiler: Brilliantness! Forcefully ramming beartype.claw down the throat of your Vim-adjacent magnum opus is an inspired decision. I applaud. Seriously, I'm both humbled and honoured that you'd even consider @beartype for eventual inclusion in qutebrowser.

This also gives me a reasonable release milestone for @beartype 0.15.0. Specifically, @beartype 0.15.0 will be ready when beartype.claw at least superficially supports qutebrowser without raising exceptions. Warnings – even many, many warnings – are only to be expected. Exceptions, however, are right out. Which brings me to...

...the fascinating traceback you submitted for your minimal-length pkg.x example. Unreadable errors like KeyError: '__main__' are obvious bad news. Resolving that is now my immediate priority. Let's do this, bear bois! ⌨️ 🐻 ⌨️

leycec added a commit that referenced this issue Jul 10, 2023
This commit is the next in a commit chain resolving all outstanding
issues with our currently unpublished `beartype.claw` API, en-route to
resolving feature request #43 kindly submitted a literal lifetime ago by
@zeevox Beeblebrox the gentle typing giant. Specifically, this commit
resolves yet another horrible microissue discovered by @qutebrowser
maestro @The-Compiler (Florian Bruhin) preventing `beartype.claw`
from properly hooking the **main module** (i.e., the first entry-point
imported into the active Python interpreter, typically via a command
resembling `python -m {muh_package}.{muh_module}`). Okay... so, that's
actually a megaissue. All signs are green for community testing by the
unruly mob squatting on the front lawn outside the filthy bear cave.
Clean this cave, unruly mob! (*Bulbous Mandelbulb in a flighty lightbulb!*)
@leycec
Copy link
Member Author

leycec commented Jul 10, 2023

@The-Compiler: Commit 15291ba resolves the unseemly pkg.x exception you kindly exposed above. But there are still soooooooo many outstanding issues here that it's not really worth anyone's precious lifeforce to test this commit.

Up Next, @leycec Self-flagellates Himself

...is what I'd say if I had an itchy burlap sack with which to self-flagellate myself. Thankfully, I don't. Therefore, my next action item is to instead hack on dataclasses.field() integration.

As @gabrieldemarmiesse wisely observed, the standard dataclasses module probably doesn't yet support typing-friendly syntax like muh_field = typing.Annotated[{muh_type}, field(...)]. I'm unclear on this point, because I am lazy and tired and failed to actually test that. Interestingly, the third-party dataclass-wizard package does already support that syntax:

from dataclasses import dataclass, field
from typing import Union
from typing_extensions import Annotated
from dataclass_wizard import property_wizard

@dataclass
class Vehicle(metaclass=property_wizard):
    wheels: Annotated[Union[int, str], field(default=4)]  # <-- woah

Still, nobody should have to require yet another third-party dependency just to get dataclasses.field() to type-check as expected. Instead, @beartype 0.15.0 will support dataclasses.field() type hints out-of-the-box. Sharpen those claws, @beartype! 🐾 🐻 🐾

@gabrieldemarmiesse
Copy link

@leycec I haven't tested yet, but it's very likely that you will get similar errors with sqlalchemy annotations, they use a similar syntax:
https://docs.sqlalchemy.org/en/20/orm/extensions/mypy.html

class Address(Base):
    __tablename__ = "address"

    id = Column(Integer, primary_key=True)
    user_id: int = Column(ForeignKey("user.id"))

    user: User = relationship(User)

@leycec
Copy link
Member Author

leycec commented Jul 10, 2023

ohgodsohgods. Head-pounding pain continues and @leycec continues clutching his head. 🤧

Thanks so much for that rapid heads-up, though. As always, @gabrieldemarmiesse delivers! And what is it with all these popular APIs (including the standard @dataclasses.dataclass decorator) that violate PEP standards, anyway? Are typing-driven IDEs like VSCode and PyCharm really okay with PEP-noncompliant type hints or did they just hard-code special cases for each of these APIs?

I suspect the latter. If ignorance is bliss, I now wish to become re-ignorant.

@leycec
Copy link
Member Author

leycec commented Jul 10, 2023

Fascinating. At least in the case of SQLAlchemy, it would thankfully seem that SQLAlchemy's older PEP-noncompliant syntax is officially deprecated and to be removed shortly:

Deprecated since version 2.0: The SQLAlchemy Mypy Plugin is DEPRECATED, and will be removed possibly as early as the SQLAlchemy 2.1 release. We would urge users to please migrate away from it ASAP.

Instead, SQLAlchemy now mandates use of supposedly PEP-compliant type hints ala:

from typing import List
from typing import Optional
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user_account"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String(30), nullable=False)
    fullname: Mapped[Optional[str]] = mapped_column(String)
    addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user")


class Address(Base):
    __tablename__ = "address"

    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    email_address: Mapped[str] = mapped_column(String, nullable=False)
    user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"), nullable=False)
    user: Mapped["User"] = relationship("User", back_populates="addresses")

Greatness! The only obvious question now is:

"What is the sqlalchemy.orm.Mapped[...] type hint factory?"

Is Mapped[...] something only static type-checkers are expected to understand or are those SQLAlchemy-specific type hints actually amenable to runtime type-checking – hopefully via a metaclass Mapped.__class__.__instancecheck__() dunder method like in jaxtyping and phantomtypes? In theory, it's the responsibility of SQLAlchemy to get this right. In practice, even popular frameworks like NumPy and Pandera never got this right and instead expected @beartype to do all the heavy lifting, which we then did but which exhausted us.

My face continues sighing dolefully. 😮‍💨

@The-Compiler
Copy link

The-Compiler commented Jul 10, 2023

@The-Compiler: Brilliantness! Forcefully ramming beartype.claw down the throat of your Vim-adjacent magnum opus is an inspired decision. I applaud. Seriously, I'm both humbled and honoured that you'd even consider @beartype for eventual inclusion in qutebrowser.

This also gives me a reasonable release milestone for @beartype 0.15.0. Specifically, @beartype 0.15.0 will be ready when beartype.claw at least superficially supports qutebrowser without raising exceptions. Warnings – even many, many warnings – are only to be expected. Exceptions, however, are right out.

You're humbled? I'm... uuuh... humbleder!

Note I don't have any plans to ship beartype to qutebrowser users - but I'd perhaps consider enabling it when running the testsuite. For now, it all remains a giant experiment!

As for...

Thanks so much for that rapid heads-up, though. As always, @gabrieldemarmiesse delivers! And what is it with all these popular APIs (including the standard @dataclasses.dataclass decorator) that violate PEP standards, anyway? Are typing-driven IDEs like VSCode and PyCharm really okay with PEP-noncompliant type hints or did they just hard-code special cases for each of these APIs?

I have no idea what sqlalchemy's Mapped is exactly (apparently, it's a SQLORMExpression[_T], an ORMDescriptor[_T], a _MappedAnnotationBase[_T] and a roles.DDLConstraintColumnRole - that explains things!).

However, as for dataclass.field, I believe this used to be hardcoded in a mypy plugin indeed. However, that changed with PEP 681 – Data Class Transforms | peps.python.org:

There is no existing, standard way for libraries with dataclass-like semantics to declare their behavior to type checkers. To work around this limitation, Mypy custom plugins have been developed for many of these libraries, but these plugins don’t work with other type checkers, linters or language servers. They are also costly to maintain for library authors, and they require that Python developers know about the existence of these plugins and download and configure them within their environment.

and apparently a @dataclass_transform decorator has the ability to specify Field specifiers, with some standardized arguments which are probably meant for type checkers to inspect accordingly.

Welp. Maybe you had a "this shouldn't be too hard!" moment when starting beartype, as I had with qutebrowser when I started it. I, for one, commend your efforts on competing with the big type checkers in this space!

@gabrieldemarmiesse
Copy link

My opinion is my own on this, but I would totally understand if the first version of beartype.claw didn't support those weird default argument for dataclasses. A workaround is available for users in every library and raising awareness about this very common bad typing pattern seems like a good thing to me.

If no one complains about this issue about dataclasses, it will never change and it will continue to confuse newcomers and type checker maintainers forever.

I have very high hopes for beartype.claw as a CI tool. And if it gets the success it deserves, I believe people will change their code so that the type hints are actually correct.

In our work codebase I already fixed 50+ type errors thanks to beartype.claw and 3 real bugs. This has a lot of potential. Mypy has too many false positive, beartype.claw has none.

Fastapi is actively pushing for this, I believe pydantic will too, we can also do our part to raise awareness by raising an exception and provide a very helpful error message with examples and documentation links.

If this goes well, the standard library dataclasses can even deprecate thefield function in favor of Field thus simplifying their api and their documentation (and being less confusing for newcomers).

@leycec
Copy link
Member Author

leycec commented Jul 10, 2023

Wowza! Thank you both so much, @The-Compiler and @gabrieldemarmiesse, for being so awesome and accepting of @beartype's rapidly accumulating pile of festering bugs that suspiciously resemble alien facehuggers when you squint at them shortly before running.

I spent the better part of a dangerous bicycle trip through the uncharted Canadian wilderness contemplating this topic rather than watching for man-eating fishers and wolverines. My conclusion is... beartype.claw needs to temporarily become dumber before it eventually becomes smarter (in several decades after the inevitable development of TypingGPT, of course).

beartype.claw is currently trying to be "smart" by transparently treating annotated class field declarations as both syntactically and semantically equivalent to annotated variable assignments. That is to say, @beartype currently treats muh_field and muh_local in the following example as equivalent:

muh_local: int = 0xBABECAFE  # <-- is this...

from dataclasses import dataclass
@dataclass
class MuhDataclass(object):
    muh_field: int = field(default=0xCAFEBABE)  # <-- ...*really* the same as this?

From the low-level perspective of abstract syntax tree (AST) transformations fuelled by the secretive and deadly art of importlib.machinery, those two things are equivalent; the annotated class field declaration for muh_field is at least syntactically equivalent to the annotated variable assignment for muh_local. In particular, they're both governed by the ast.AnnAssign node and corresponding ast.NodeTransformer.visit_AnnAssign() method.

From the higher-level perspective of sanity, semantics, typing, and "What does this actually mean?", however, we can agree that those two things are actually not at all alike and probably shouldn't be treated as such. What I am trying to say here is that the next commit will explicitly ignore annotated class field declarations when applying beartype.claw import hooks. This should get us much closer to where we want to be, which is a working beartype.claw API.

B-b-but... What About Type-checking Dataclass Fields?

Yup. Type-checking dataclass fields is absolutely something that a future version of @beartype should do. Indeed, @beartype 0.14.1 already type-checks dataclass fields at __init__() time; all that's left is to type-check dataclass fields at assignment time for non-frozen dataclasses.

But that's sorta outside the scope of beartype.claw. Let's leave this as an exercise to the reader with too much time and too little sanity. So, none of us. Prepare for incoming commits! 😑 🤯 💥

leycec added a commit that referenced this issue Jul 11, 2023
This commit is the next in a commit chain resolving all outstanding
issues with our currently unpublished `beartype.claw` API, en-route to
resolving feature request #43 kindly submitted a literal lifetime ago by
@zeevox Beeblebrox the gentle typing giant. Specifically, this commit
*hopefully* resolves a veritably vomitous slew of horrible microissues
simultaneously uncovered by both @qutebrowser maestro @The-Compiler
(Florian Bruhin) *and* Python-on-Whales mastermind @gabrieldemarmiesse
(Gabriel de Marmiesse) preventing `beartype.claw` from properly hooking
modules containing **dataclasses** (i.e., PEP 557-compliant dataclasses
decorated by the standard `@dataclasses.dataclass` decorator as well as
PEP 681-compliant third-party dataclasses decorated by the equally
standard `@typing.dataclass_transform` decorator) that contain one or
more **annotated dataclass fields** (i.e., class variables whose values
are instances of the `dataclass.field` type and whose types are
annotated with PEP-compliant type hints). If anyone actually read this,
please confirm with an animated GIF of a jiggling bear on the @beartype
issue tracker. (*Hospitable spit on a hospital table!*)
@leycec
Copy link
Member Author

leycec commented Jul 11, 2023

Ah-ha! Rejoice, bear fellows, for it is Summer. The Sun is sweltering, the water is warm, and GitHub is banging. @leycec's back with a brand new commit promising to resolve some – possibly even many, but doubtfully all – of your pressing issues with the oppressive beartype.claw API.

Let's type this:

pip install git+https://github.com/beartype/beartype.git@7bfa5c61598a21b6024ba1ab43a0a0bc76818b88

Specifically, this commit relaxes @beartype's prior death-grip on dataclasses. Whether standard @dataclasses.dataclass classes or third-party dataclasses from Pydantic or SQLAlchemy, beartype.claw should now transparently support your data without barfing over itself.

Back to the fun stuff! @The-Compiler, you said this fun stuff...

Note I don't have any plans to ship beartype to qutebrowser users

ohthankgods

I was sweating bullets there, thinking that I'd accidentally destroyed my favourite browser by my own ill-gotten hands.

Dear beloved users: Please do not embed beartype.claw in production code just yet. @beartype 0.15.0 will be strenuously tested against all possible edge cases you are currently discovering for me. But... the real world is a nastier, grimier, and filthier place than even we can conceive. There are chromatic dragons only seen by mantis shrimp that will raze and burn your codebases to the ground as management justifiably roars in inchoate rage.

Of course, I myself will violate my own sound advice by embedding beartype.claw in production code shortly after releasing @beartype 0.15.0. Honestly, I'm just kinda done with manually decorating everything by @beartype. Gets old after awhile, doesn't it? That's why we're all here. Laziness triumphs over carpal tunnel syndrome.

...but I'd perhaps consider enabling it when running the testsuite. For now, it all remains a giant experiment!

This is the way. beartype.claw is explicitly compatible with pytest. Indeed, beartype.claw itself is tested entirely with native pytest tests. Indeed, I'm pretty sure beartype.claw is the only import hook API that is compatible with pytest. Let's pretend I know what I'm talking about.

Welp. Maybe you had a "this shouldn't be too hard!" moment when starting beartype, as I had with qutebrowser when I started it.

I am still having that moment. 🤣 🤭 😭

It all seemed so simple, once. "Just a hundred lines of code, innit?" I said. "What could possibly go wrong?" they said. I am here to tell you that some dreams are better left under the pillow.

Qutebrowser also probably seemed so simple, once. By harnessing the ferocious browser power of QtWebEngine + PyQt, your destiny to make the ultimate browser was already in reach. Or so it seemed... Cue orchestral score in the opening credits to "Qutebrowser: The True Story of One Man's Titanic Struggle."

Honestly, I was incredibly impressed by qutebrowser a decade ago. I'm even more impressed now. Against all economic odds, against the mainstream grain, you've hand-built a part-time – and possibly someday full-time – career around your life's passion. You win GitHub. </big_applause>

I, for one, commend your efforts on competing with the big type checkers in this space!

They will all submit to the Bear! Okay. They probably won't. We'll probably be left biting the dust of their exhaust trails as the proudly jet ahead under profitable corporate and non-profit funding models while I toil quietly in the dark man cave that has yet to be cleaned in over a decade.

But... seriously. I can't stand those big type-checkers. They may be big, but they either lie profusely about everything (mypy, pyright) or tell the truth but are so enfeebled with worst-case inefficiency (typeguard) that your continuous integration workflow dissolves into a froth of aborted processes as the wall-clock minutes churn into hours. This is why I @beartype.

I'm imagining you trying to use vanilla Firefox or Chromium on an unfamiliar laptop at the family snow chalet in the Swiss Alps, @The-Compiler – only to curse the sky as muscle memory and cherished keyboard shortcuts all fail to do anything. This is why you qutebrowser.

More fun stuff! @gabrieldemarmiesse, you said this fun stuff...

My opinion is my own on this, but I would totally understand if the first version of beartype.claw didn't support those weird default argument for dataclasses.

I share your correct opinion. @beartype users are usually right about everything. This is why beartype.claw now ignores field declarations in all classes, ...including dataclasses because it's basically infeasible to differentiate between standard classes and dataclasses from the low-level perspective of AST transformations in the beartype.claw API.

You are right about everything is what I'm saying.

I have very high hopes for beartype.claw as a CI tool.

Awwwww! You so nice, Gabriel. These sweet words are like a torrent of optimism jacked directly into my brainstem. 🤗

And if it gets the success it deserves, I believe people will change their code so that the type hints are actually correct.

Yes! Change your incorrectly typed code, people. @beartype demands politely requests this.

In our work codebase I already fixed 50+ type errors thanks to beartype.claw and 3 real bugs.

😮

...wow. That many? That's amazing. Well, horrifying. But also amazing! I'm delighted that beartype.claw has already yielded something positive for someone – and it's not even officially out or even working fully debugged yet.

This has a lot of potential. Mypy has too many false positive, beartype.claw has none.

Right? Riiiiiiight!? My wife refuses to even touch mypy or pyright, because "They waste my time." But she does religiously decorate everything with @beartype. "But isn't that a waste of time, too?", I mutter to myself. When I suggested she no longer needs to do that, she exclaimed: "Over my dead and twitching body!"

I feel like my wife has an unhealthy attachment to the @beartype decorator.

Thanks for all the laughs, everybody. We'll all summit the mountain of a better, bolder, safer Python world... together. muhahahahah— gurgling sounds as @leycec passes out

@The-Compiler
Copy link

The-Compiler commented Jul 11, 2023

Welp, I believe I ran into another interesting corner case - here is a minimal example:

from collections.abc import Iterator
from contextlib import contextmanager
from beartype import beartype

@beartype  # this breaks, and is what beartype.claw does
@contextmanager
# @beartype  # this works
def ctx() -> Iterator[None]:
    yield

with ctx():
    pass

which results in (newlines inserted for readability):

Traceback (most recent call last):
  File ".../test.py", line 11, in <module>
    with ctx():
  File "<@beartype(__main__.ctx) at 0x7f6e76793be0>", line 19, in ctx
beartype.roar.BeartypeCallHintReturnViolation: Function __main__.ctx()
   return <contextlib._GeneratorContextManager object at 0x7f6e767a1570>
   violates type hint collections.abc.Iterator[None],
   as <protocol ABC "contextlib._GeneratorContextManager"> <contextlib._GeneratorContextManager object at 0x7f6e767a1570>
   not instance of <protocol ABC "collections.abc.Iterator">.

Which is true when looking at the decorated function. However, we're annotating the undecorated one. Moving the @beartype to after (lines-wise) before (time-wise, as Python runs decorators from innermost to outermost) @contextmanager fixes this, but obviously that isn't possible with beartype.claw.

@leycec
Copy link
Member Author

leycec commented Jul 11, 2023

Blarghitty! Your fascinating example is causing my body to sweat with anxiety. Ordinarily, that would be bad. But thanks to the extreme Canadian heat wave that has caused even the cats to put their paws up in the air, I'm grateful. This cottage was built before air conditioning was invented.

So. You have uncovered an ordering issue in decoration chaining. That's awful. @beartype will probably need to accommodate this like it does similar ordering issues in decorating chaining with the standard @classmethod, @property, and @staticmethod decorators – namely, by forcefully reversing the order of decoration.

Thankfully, this means that this is actually an issue with the @beartype decorator rather than the beartype.claw API. Ideally, @beartype should:

  1. Detect when @beartype is chained after @contextlib.contextmanager.
  2. When this is detected:
    1. Get the original callable decorated by @contextlib.contextmanager.
    2. Decorate that (rather than the callable created by @contextlib.contextmanager) by @beartype.
    3. Re-decorate the callable created by @beartype by @contextlib.contextmanager, effectively "throwing away" the prior callable created by @contextlib.contextmanager.

This implies a bit of decoration-time inefficiency. But... we currently do that with @classmethod, @property, and @staticmethod decorators and nobody particularly minds. Except they will, now that I've publicly admitted to our crimes. 😅

I've opened a new feature request for this. Make the pain go away, issue tracker...

@leycec
Copy link
Member Author

leycec commented Jul 19, 2023

Feels good. We're there. We're finally there. 😌

I mean, we're probably not actually there. We're probably still years away from there. But beartype.claw is now being used by downstream consumers in continuous integration (CI) pipelines, which means it is time to ship this before those pipelines erupt in disaster.

The tentative gameplan is to:

Prepare for incoming beartype.claw. We doin' this.

@gabrieldemarmiesse
Copy link

which means it is time to ship this before those pipelines erupt in disaster.

Hey no pressure, we are all consenting adults here, we know the consequences of what we're doing haha :)

@gabrieldemarmiesse
Copy link

I think we may want to get involved in this discussion about annotated and dataclasses: https://discuss.python.org/t/dataclasses-make-use-of-annotated/30028/15

@leycec your feedback here will likely be very appreciated

@leycec
Copy link
Member Author

leycec commented Jul 22, 2023

Thanks so much for pinging me on, @gabrieldemarmiesse. I should definitely weigh in. You're so right. But I have deep bags under my weary eyes and @beartype 0.15.0 is due to be released tonight, so... the exhaustion is real. 😩

I'm utterly shocked shocked, I say! that there's nonsensical pushback from all of the usual suspects for a sensible improvement to the existing @dataclass decorator. This is why I tend to avoid the PEP standardization process. Clearly, there's no tangible drawback to supporting Annotated[..., field(...)] as a permissible value for @dataclass fields – only positive gains. Doing so (A) better aligns @dataclass with third-party dataclass frameworks like Pydantic and (B) improves compatibility with existing typing standards like PEP 484 and 526. This should be obvious to everyone.

I am facepalming myself on a Friday night.

@eli-schwartz
Copy link

Nifty work. I too have been somewhat interested in runtime type checking as a CI-time sanity check of my codebase, but I have hit an issue that seems difficult and possibly somewhat contradictory to work around.

Specifically, we make pretty heavy use of imports guarded by if typing.TYPE_CHECKING to prevent import circularity and skip expensive but not always needed imports in the garden path. This understandably makes beartype sad, because it cannot see that code. Hacking it with typing.TYPE_CHECKING = True under the rationale that "running optional extra type checking is a good reason to do the expensive imports" does not help either as that simply results in partially initialized modules that fail to import.

mypy can be a dirty cheater and just analyze every file without worrying about what point in time each reference becomes resolvable, or actually trying to execute the results. I'm not sure what the options are for a runtime checker...

@leycec
Copy link
Member Author

leycec commented Jul 24, 2023

Gah! I totally neglected to account for typing.TYPE_CHECKING shenanigans while testing beartype.claw. I feel shame. Thankfully, you're not the only one to hit that obvious minefield; please see issue #259, where everybody else face-planted into the same deadly trap too.

As an obvious workaround that you've almost certainly considered and already given up on, would simple forward references (e.g., def muh_func(expensive_type: 'muh_package.muh_module.muh_expensive_type'): ... suffice for your use case? @beartype supports simple forward references as a means of circumventing circular import dependencies, partially initialized modules, and other chicken-and-egg conundrums. Since we're having this discussion, though, I suspect you need a more powerful bazooka than that. Typing in the real world is indeed a lesson in pain and futility. 🤕

@tolomea
Copy link

tolomea commented Jul 24, 2023 via email

@eli-schwartz
Copy link

As an obvious workaround that you've almost certainly considered and already given up on, would simple forward references (e.g., def muh_func(expensive_type: 'muh_package.muh_module.muh_expensive_type'): ... suffice for your use case?

I suppose the hope here is that rooting in the top-level namespace means you can optimistically use names that haven't actually been imported, because if e.g. muh_module were imported by any other file, then as far as sys.modules is concerned, muh_package.muh_module exists and is a valid attribute of sys.modules['muh_package'] regardless.

There are two problems with this:

  • Dotted names tend to become quite long. That single annotation, for example, is 41 characters, taking up quite a lot of valuable cognitive real estate.
  • Currently, the codebase in question uses relative imports. In theory this could be rewritten.

@eli-schwartz
Copy link

Since we're having this discussion, though, I suspect you need a more powerful bazooka than that.

When trying to imagine what "a more powerful bazooka" might look like, the only thing that immediately leaps to mind is an import hook that parses if typing.TYPE_CHECKING blocks, saves references to all tokens defined in it (whether via imports, assignments, class Foo(typing.TypedDict), or what have you) into a forward reference cache, and knows how to backtrack and re-evaluate the associated python statements on demand (at runtime type checking time). Sounds hairy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants