Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Beartype 0.18.3: For Justice, For Victory, For QA
Beartype 0.18.3 is the minor patch release that your careening codebase can no longer live without: ```bash pip install --upgrade beartype ``` Actually... *I lied.* I know! I gotta stop doing that. But the sordid truth is that beartype 0.18.3 is mostly just for @iamrecursion and @sylvorg, who single-handedly reported more issues in a single week than the exploding size of @leycec's JRPG backlog. And we know how big *that* is, don't we? It's big. It's so big it wraps around like a self-sustaining Niven ringworld habitat at Lagrange point L1. Big-big. Beartype 0.18.3 is for @iamrecursion and @sylvorg. May their usernames live forever in `git log` infamy. In this release, a few more bugs die. But first... ## GitHub Sponsors: They Scratch the Bear's Back. Now, The Bear Scratches Back. This release comes courtesy these proud [GitHub Sponsors](https://github.com/sponsors/leycec), without whom @leycec's cats would currently be eating grasshoppers: * @sesco-llc (SESCO Enterprises), "The Power of Innovation in Trading": <sup>*this inspires me to get out of the house and do something*</sup> https://sescollc.com * @DylanModesitt (Dylan Modesitt), quantitative strategies energy trading associate: <sup>*...wikipedia, don't fail me now!*</sup> https://dylanmodesitt.com * @tactile-metrology (Tactile Metrology), "Software and hardware that you can touch." When I want to be touched by software and hardware, I call @tactile-metrology: https://metrolo.gy <sup>imagine if this domain actually worked. *how cool would that be!?*</sup> Thanks so much, masters of fintech and metrology. ![](https://media1.tenor.com/m/7JR7sD8oTiUAAAAC/who-are-you-do-i-know-you.gif) <sup>The Masters of Fintech and Metrology. *That's who.*</sup> ## Type Variables Bound by Forward References: So This Is a Thing, Huh? **So.** Funny story. Turns out you can bound PEP 484-compliant **type variables** (i.e., `typing.TypeVar(...)` objects) with forward references specified as strings. Who knew? Everybody except @leycec. Nobody tells that guy nuthin'. Beartype 0.18.3 now explicitly supports **type variables bound by forward references.** These are type hints of the form: ```python TypeVar('{TypeVarName}', bounds='{UndefinedType}') ``` Previously, @beartype only partially supported such variables due to @leycec failing to realize that such variables even existed and constituted a valid use case. This is why your codebase can't have good things. Now, @beartype fully supports ~~heinous abominations~~ valid use cases like: ```python from beartype import beartype from typing import TypeVar # Type variable bound by a forward reference. @beartype supports your # weird stuff, because normalcy is just a null pointer to garbage. Fuggit = TypeVar('Fuggit', bound='EnduringMisery') @beartype class EnduringMisery(object): def searing_pain(self) -> tuple[Fuggit, Fuggit]: return ('Fuggit...', '...up!') # Can anyone guess what this does? That's right. It blows up. Fuggit! blinding_agony = EnduringMisery() blinding_agony.searing_pain() ``` ![](https://media1.tenor.com/m/bOaLkBpLVtoAAAAd/joker-persona5.gif) <sup>*hangin' with the bear homies in apocalyptic wasteland ain't no thang*</sup> ## `beartype.vale.Is[...]`: Now It Supports Crazy Stuff The **functional beartype validator factory** `beartype.vale.Is[...]` is now subscriptable (indexable) by all manner of shambolic nightmares. Previously, you had to subscript `Is[...]` with low-level functions and methods. Now, you can subscript `Is[...]` with high-level callable objects like: * **Class-based callables** (i.e., objects whose classes define the `__call__()` dunder method, rendering otherwise uncallable objects callable): e.g., ```python from beartype.door import is_bearable from beartype.typing import Annotated from beartype.vale import Is from functools import partial class TruthSeeker(object): def __call__(self, obj: object) -> bool: ''' Tester method returning :data:`True` only if the passed object evaluates to :data:`True` when coerced into a boolean and whose first parameter is ignorable. ''' return bool(obj) # Beartype validator matching only objects that evaluate to "True". Truthy = Annotated[object, Is[TruthSeeker()]] assert is_bearable('', Truthy) is False assert is_bearable('Even lies are true now, huh?', Truthy) is True ``` * **Partials** (i.e., high-level `functools.partial(...)` callable objects wrapping low-level functions and methods): e.g., ```python from beartype.door import is_bearable from beartype.typing import Annotated from beartype.vale import Is from functools import partial def is_true(ignorable_arg, obj): ''' Tester function returning :data:`True` only if the passed object evaluates to :data:`True` when coerced into a boolean and whose first parameter is ignorable. ''' return bool(obj) # Partial of the is_true() tester defined above, effectively ignoring the # "ignorable_arg" parameter accepted by that tester. is_true_partial = partial(is_true, 'Gods. This code is literally unreadable.') # Beartype validator matching only objects that evaluate to "True". Truthy = Annotated[object, Is[is_true_partial]] assert is_bearable('', Truthy) is False assert is_bearable('Even lies are true now, huh?', Truthy) is True ``` Is this valuable? No idea. Let's pretend I did something useful tonight so I can sleep without self-recrimination. ![](https://media1.tenor.com/m/G4tOyILs1nkAAAAC/shuake-good-morning.gif) <sup>*...heh.* your eyes are now bleeding</sup> ## Triply-Redeclared Types: *Just don't ask.* Beartype 0.18.3 now sports improved support <sup>*we rhymin' like it's 2099 ova here*</sup> for **Jupyter Notebook cells.** Do you like Jupyter? Do you like @beartype? Then you need beartype 0.18.3 now, because beartype 0.18.2 probably already broke everything without your informed consent. *Woops.* Beartype 0.18.3 resolves inscrutable non-determinism (which is technically deterministic if you squint at it, but we don't talk about that) with respect to repeatedly redefined classes defining one or more methods annotated by one or more **self-referential relative forward reference** (i.e., referring to the class currently being defined). @beartype is now *considerably* more robust against non-determinism in Jupyter cells containing `@beartype`-decorated self-referential classes like: ```python from beartype import beartype @beartype class MuhSelfReferentialClass(object): def __init__(self, muh_var: int) -> None: self.muh_var = muh_var @classmethod def muh_factory(cls, muh_var: int) -> "MuhSelfReferentialClass": ''' This is fine now. No matter how much you reload the cell defining this class, @beartype will still stan for you. I have no idea what "stan" even means. I think it's good. ''' return MuhSelfReferentialClass(muh_var + 42) muh_object = MuhSelfReferentialClass.muh_factory(42) ``` Flex those burly QA biceps, @beartype. Flex 'em. ![](https://media1.tenor.com/m/8rO7cAVs3UMAAAAC/yusuke-kitagawa-persona5.gif) <sup>things explode when you put @beartype back in the sheath</sup> ## `__class_getitem__ = classmethod(GenericAlias)`: We Do That Too, Whatever That Is **So.** You want to refactor your heroic class that will truly shape the course of human history into a subscriptable type hint factory. You even know about the convenient but unreadable one-line idiom for casting this dark magic. Previously, @beartype refused to support your ~~bad habits~~ arcane knowledge. Now, @beartype understands and appreciates everything you're trying to do for humanity. Beartype 0.18.3 generalizes the `@beartype` decorator to support decoration of user-defined types that declare class methods by directly calling the builtin `@classmethod` decorator as a function passed a C-based callable type (e.g., `classmethod(types.GenericAlias)`). Doing so enables `@beartype` to support the standard idiom for user-defined subscriptable type hint factories under Python >= 3.9: ```python from abc import ABCMeta from beartype import beartype from types import GenericAlias @beartype class MuhTypeHintFactory(metaclass=ABCMeta): ''' Congrats. Subscripting this class now trivially makes new type hints that @beartype fails to understand or appreciate. ''' # This exact one liner appears verbatim throughout the standard # library as well as popular third-party packages like NumPy. __class_getitem__ = classmethod(GenericAlias) # Not sure what this means, but you insist you know what you're doing. # *Do* you, though? *Do* you? @beartype is out to lunch on this one. MuhTypeHint = MuhTypeHintFactory[str] ``` ![](https://media1.tenor.com/m/BYlpl5VB6EkAAAAC/persona-5-goro-akechi.gif) <sup>*the pancakes get me every time.* srsly. what is with those pancakes?</sup> ## Forward Reference Deprioritization: What Does This Even Mean!? Beartype 0.18.3 deprioritizes @beartype-specific **forward reference proxies** (i.e., internal objects proxying external user-defined types that have yet to be defined) in type tuples passed as the second arguments to the `isinstance()` builtin, reducing the likelihood that type-checks involving forward references will raise unexpected exceptions. For example, consider this simple example: ```python from beartype import beartype from beartype.typing import Union @beartype def explosive_funk(muh_arg: Union['UndefinedType', None] = None): print("You thought this was gonna blow up, huh? You're not alone.") print("Unless you're in space. In which case you're really alone.") explosive_funk() class UndefinedType(object): ... ``` ...which unexpectedly prints *without* blowing up: ``` You thought this was gonna blow up, huh? You're not alone. Unless you're in space. In which case you're really alone. ``` @beartype type-checks that the default value of the optional `muh_arg` parameter of the `muh_func()` function satisfies the type hint `'UndefinedType' | None` – despite the fact that the `UndefinedType` class is undefined! To do so, @beartype now internally reorders the types comprising this union: ```python # ...from this default type-check, which would raise a decoration-time # exception due to "UndefinedType" being undefined... isinstance(muh_arg, (UndefinedTypeProxy, NoneType)) # ...to this default type-check, which should raise *NO* decoration-time # exception. Why? Because the default value "None" for the "muh_arg" # parameter satisfies the first "NoneType" type, which then # short-circuits the isinstance() call and thus ignores the problematic # "UndefinedTypeProxy" type altogether. isinstance(muh_arg, (NoneType, UndefinedTypeProxy)) ``` **Nobody should ever depend upon this.** Therefore, this is a delicious nothingburger – but a delicious nothingburger that could yield future delights in the event that we actually elect to try type-checking defaults at decoration time again. We're not, of course. That would be foolish and dangerous. <sup>We're absolutely going to do that again.</sup> ![](https://media1.tenor.com/m/hKOXQloPIgMAAAAC/futaba-persona5.gif) <sup>rub those cat cheeks! rub 'em!</sup> ## PEP 563 + PEP 673 + dunder methods. Beartype 0.18.3 resolves a subtle interaction between PEP 563 (i.e., `from __future__ import annotations`), PEP 673 (i.e., `typing{_extension}.Self`), and common dunder methods like... uh, `__add__()`, I guess. Let's pretend that's common. Beartype 0.18.3 ensures that the type stack encapsulating the current `@beartype`-decorated class is now preserved throughout the type-checking process for standard dunder methods annotated by one or more PEP 673-compliant `typing{_extension}.Self` type hints that are stringified under PEP 563. For example, @beartype now transparently supports pernicious edge cases resembling: ```python from beartype import beartype from typing_extensions import Self @beartype class MyClass: attribute: int def __init__(self, attr: int) -> None: self.attribute = attr def __add__(self, other: int) -> Self: self.__class__(self.attribute + other) ``` If you wanted this, you are literally @iamrecursion. Congrats. ![](https://media1.tenor.com/m/LM2gxeKVa0IAAAAC/p5-persona.gif) <sup>you too will believe that @beartype 0.18.3 actually works</sup> ## Ping 'Em All Pinging @posita, @iamrecursion, @sylvorg, @tactile-metrology, @kalaspuff, @danielward27, @kloczek, @uriyasama, @danielgafni, @JWCS, @rbroderi, @AlanCoding, @tvdboom, @crypdick, @WeepingClown13, @RobPasMue, @rbnhd, @radomirgr, @rbroderi. You are wanted on floor 13. Japanese buildings don't even *have* a floor 13. Surely nothing could go wrong by violating that fundamental. ![](https://media1.tenor.com/m/ffGtDU9FVdAAAAAC/persona5-joker.gif) <sup>*those dance moves can mean only one thing...* This was @beartype 0.18.3.</sup>
- Loading branch information