Repository: beartype/beartype

Issue #364: [Feature Request] Generalize `beartype.door.{is_bearable,die_if_unbearable}` to dynamically resolve relative forward references, even if doing so is slower than watching paint dry :smile: 
Created: 2024-04-11T16:10:29Z
Starting with the latest beartype version I started getting this error:
beartype.roar.BeartypeDecorHintForwardRefException: Die_if_unbearable() type hint typing.Dict[str, ForwardRef('XXXX')] type hint relative forward reference "XXXX" currently only type-checkable in type hints annotating @beartype-decorated callables and classes. For your own safety and those of the codebases you love, consider canonicalizing this relative forward reference into an absolute forward reference (e.g., replace "XXXX" with "{your_package}.{your_submodule}.XXXX").
The suggested solution to this error is to change the format of our ForwardRefs. Sadly not all ForwardRefs are inside our codebase, and this is highly limiting.
I suggest Adding a "local_classes" param to beartype.door functions which maps ForwardRef string to the class itself.
Handcrafted example where this issue arises.
from typing import Dict
from dataclasses import dataclass

@dataclass
class Settings:
    overrides: Dict[str, "Settings"]

if __name__ == "__main__":
    a = {"hello": Settings(overrides={})}
    die_if_unbearable(a, Dict[str, "Settings"])```
    
This ticket follows #356

Duplicate issue #414: [Bug] Subscripted generic forward references unsupported
Created: 2024-08-13T21:53:44Z
Currently beartype.door.{is_bearable,die_if_unbearable} doesn't seem to be able to accept forward references when they have a generic type. Consider the following:
from collections.abc import Sequence
from typing import Generic, TypeVar

from beartype.door import is_bearable

T = TypeVar('T')
class Foo(Generic[T]):
  ...

class Bar(Sequence['__main__.Foo[float]']):  # if i remove "[float]" this entire code works
  ...

bar = Bar()
is_bearable(bar, Bar)  # Throws BeartypeDecorHintForwardRefException
yields the error:
beartype.roar.BeartypeDecorHintForwardRefException: Forward reference '__main__.Foo[float]' not valid Python attribute name.

I get this seems similar to #364 and I figure solving #364 would hopefully entail solving this, but if I understand correctly, thats pretty far away. And I just can't seem to find a good work around for this one. So other insight to how to solve this temporarily would also be greatly appreciated.

Closing comment (2024-08-14T07:17:58Z): ...heh. Forward reference resolution in is_bearable(), huh? Sounds great! I definitely should have done that... like, decades ago. There's actually no significant blocker to me doing that. Well, okay. The only significant blockers to me doing that are sanity and time – and I probably still have copious amounts of both of those. Probably.
Technically, this is indeed a duplicate of #364. Please prod me if I fail to promptly implement #364 for you and everybody else who kinda desperately needs this. I didn't know is_bearable() was in such hot demand. Thanks so much for the friendly heads-up. 👍 😸 👍
In the meanwhile, you might be able to temporarily circumvent this horrible deficit in @beartype with an equally horrible typing.TYPE_CHECKING hack like:
from beartype.door import is_bearable
from collections.abc import Sequence
from typing import TYPE_CHECKING, Iterator, TypeVar

T = TypeVar('T')

# If this module is currently being statically type-checked,
# prefer a full-blown subscripted generic forward reference.
# Sadly, is_bearable() currently fails to support this. Sad!
if TYPE_CHECKING:
    _BarSuperclass = Sequence['__main__.Foo[float]']
# Else, this module is being interpreted by a Python interpreter.
# In this case, reduce this to an unsubscripted generic forward
# reference that @beartype can actually handle.
else:
    _BarSuperclass = Sequence['__main__.Foo']

class Bar(_BarSuperclass):
    def __init__(self, items: list) -> None: self._items = items
    def __contains__(self, item: object) -> bool: return item in self._items
    def __iter__(self) -> Iterator: return iter(self._items)
    def __len__(self) -> int: return len(self._items)
    def __getitem__(self, index: int) -> object: return self._items[index]
    def __reversed__(self) -> Iterator: return reversed(self._items)
    def count(self, item: object) -> int: return self._items.count(item)
    def index(self, *args, **kwargs) -> int:
        return self._items.index(*args, **kwargs)

class Foo(Generic[T]): pass

bar: Bar[float] = Bar(('ugh,'))
is_bearable(bar, Bar)
Does that actually work? Probably not. Looks kinda sketchy, huh? I'm squinting at that and not liking what I'm seeing.
Let's quietly close this in favour of #364, which I really should just do already. 😮‍💨