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] New beartype.math API for performing type hint arithmetic #133

Closed
leycec opened this issue Jun 1, 2022 Discussed in #132 · 12 comments · Fixed by #136
Closed

[Feature Request] New beartype.math API for performing type hint arithmetic #133

leycec opened this issue Jun 1, 2022 Discussed in #132 · 12 comments · Fixed by #136

Comments

@leycec
Copy link
Member

leycec commented Jun 1, 2022

Discussed in #132

Originally posted by tlambert03 May 31, 2022
First, thanks for this amazing library. The scope and detail are beary impressive. 🐻

Apologies if I've missed an obvious answer (or previous discussion) of this question: I've been digging through the readme and source code for a couple days but still haven't hit on it, and I'm not sure if this is just fundamentally out of the primary goal here of isinstance(some_object, some_typehint).

I'm looking for something that would tell me whether type hint B is "compatible" with type hint A (something akin to pytypes is_subtype function):

from typing import Sequence, List, Union, Optional, Any

def is_subtype(hintA, hintB) -> bool:
    ...  # ?

assert is_subtype(list, list) is True
assert is_subtype(list, Sequence) is True
assert is_subtype(Sequence, list) is False

assert is_subtype(List[int], Sequence[Any]) is True
assert is_subtype(List[int], Sequence[int]) is True
assert is_subtype(List[int], Sequence[str]) is False

assert is_subtype(list, Union[list, str]) is True
assert is_subtype(Union[int, str], Union[int, str, list]) is True
assert is_subtype(Union[list, str], list) is False
assert is_subtype(list, Union[int, str]) is False
assert is_subtype(Union[int, tuple], Union[int, str, list]) is False

assert is_subtype(Optional[int], int) is False
assert is_subtype(int, Optional[int]) is True

is this something that could be achieved with beartype? digging through the source, i see lots of stuff that seems very useful towards this goal, but not sure I see anything in the public API (since we're not really trying to determine whether some object is an instance of a hint).

my goal here would be to detect if an API breakage has occurred between two versions:

# version 1
def func() -> list:
   ...
# version 2
def func() -> Sequence:  # potential API breakage
   ...

... and this seems like something you'd have an opinion on :)
thanks for your work!

@leycec
Copy link
Member Author

leycec commented Jun 1, 2022

Fascinating. Something spicey resembling beartype.is_subtype() is totally in @beartype's wheelhouse. Sadly, as you surmise, @beartype currently lacks a public API for testing type hint compatibility via (super|sub)set theoretic operators like ⊆ and ⊂ implemented through Python's builtin in operator – mostly because you're the first fearless bear-bro to ever request this.

my goal here would be to detect if an API breakage has occurred between two versions:

Yes! Thanks for that perfect use case as well. The real-world applicability is clear. If you've typed an existing callable as returning a type hint A, can you safely re-type that same callable as returning a type hint B while preserving backward compatibility? Equivalently, in set theoretic notation, is A ⊆ B?

So let's suppose we define this fun new beartype.is_subtype() API as we eventually should. I'm envisioning that function mostly being called interactively from Python REPLs like IPython3 or Jupyter Notebook. Is that right? Okay. So...

Let's Just Pretend Type Hints Are Sets, Guys

Scarce volunteer resources rears its ugly mullet cut yet again. Briefly inspecting the pytypes.is_subtype() tester suggests this problem domain to be really, really non-trivial. That's probably the most significant blocker here.

It sort of makes sense this would be the NP-hard of type hints, right? After all, type hints aren't sets. They're type hints. There's nothing innately set-like about type hints. So, attempting to coerce type hints into sets for the purpose of deciding superset and subset containment relations between otherwise unrelated type hints rapidly descends into some fairly intense academic theory-crafting.

Of course, we're all for theory-crafting here. @beartype is already built on a flimsy foundation of hand-waving, wishful thinking, and helpful assumptions that mostly hold up until you squint at them. Still... pytypes is dead. I can't help but think that part of the reason pytypes died is exactly this: pytypes authors devoted a shocking slice of their scarce time to grotesquely NP-hard-like problems that most of their userbase didn't particularly care about.

Let's not throw ourselves like disgruntled lemmings into that same hole. 🕳️

So What Are You Saying Here, @leycec?

Welp... I'm kinda sayin' this may never happen. I mean, I really hope it does! But it's a surprisingly hardcore feature that would strongly benefit from funding sponsorship of some sort. Lucrative grant funding agencies across the industrial world: make this happen for all humanity, please.

Let's see what the unruly rabble says. Does anyone else really want this too – or can I glibly pretend this never happened and devote myself to other h0t ticket dumpster fires that are currently on fire?

@leycec
Copy link
Member Author

leycec commented Jun 1, 2022

Originally posted by @tlambert03 May 31, 2022

lolll ... I commend your honesty 😂

I am not surprised to hear you say that. I kinda supposed it was a hardcore feature. I'll promise to try to sneak your name into any conversations I happen to have with deep-pocketed FOSS lovers 🤑

however... in the meantime, let's suppose I'm a positively gruntled lemming who's a bit obsessed with this problem at the moment, likely a bit naïve, and that you're not going to lose me that easily. (but that I also definitely don't expect to pull you so easily from your many other dumpster fires which we can all understand and empathize with).

And let's also suppose that I'm ok with thinking about this in stages. If you look at the pytypes implementation, for all its complexity and indirection, it boils down to this:

from typing import ForwardRef, TypeVar

def is_subtype(sub, sup):
    if isinstance(sub, ForwardRef) or isinstance(sup, ForwardRef): ...
        # deal with all the nastiness of ForwardRefs
    if issubclass(sub, Empty) or issubclass(sup, Empty): ...
        # where Empty is a special pytypes class trying to deal with
        # https://github.com/python/typing/issues/157
    if isinstance(sup, TypeVar) or isinstance(sub, TypeVar): ...
        # deal with __bound__, __contravariant__, __covariant__
    if is_Tuple(sup): ...
    if is_Union(sup): ...
    if is_Union(sub): ...
    if is_Generic(sup): ...
    try:
        return issubclass(sub, sup)
    except TypeError:
        return False
  • ForwardRefs are annoying, but that's a different (namespace) problem.
  • I'm ok with ignoring the Empty case for now, maybe forever.
  • TypeVar is bounds and co/contra-variants are hard, but I'd happily raise a NotImplementedError for now and likely still profit quite a bit.
  • after that, we're down to Generics – probably the bulk of the "non-standard" work
  • then the rest is "straightforward" enough.

If I were to dig into this some more on my own time – likely using some of your private API and fully expecting it to break without notice – would you have any additional "here's where I might start if I were you" sort of thoughts? (s'ok if not!) I feel like there's some gold buried in sanify_hint_root and in _util/hint/* that could get this started off on at least a nice normalized footing.

thanks already for your time and thorough response!

@leycec
Copy link
Member Author

leycec commented Jun 1, 2022

Yes, yes, and yes. While clearing the ruinous fallout of a 150 year-old oak tree that fell over during the deadliest derecho in Ontario history, crushed our bunkhouse, and then toppled onto our cottage, I had a miraculous epiphany. Praise Guido!

I didn't intend to think about this – but there I was, hauling debris, petting cats, and thinking about this despite my best intentions. But first...

What Not to Do

What not to do is what pytypes does – probably. You've spelunked deeper into the cavernous labyrinth of the pytypes codebase than I have. So, this is mere baseless speculation on my part. But...

What pytypes does is guaranteed to fail – probably. If I'm reading the pytypes.type_util submodule correctly, the pytypes.is_subtype() function basically reduces to a linear chain of if conditionals. Your superb synopsis of that function corroborates that I am not entirely insane. Sadly, a linear chain of if conditionals can't possibly suffice to cover all possible edge cases. Why? Because type hints are infinitely composable (i.e., recursively nestable). For example, consider just this simple fragment from your pytypes.is_subtype() simplification:

    if is_Tuple(sup): ...
    if is_Union(sup): ...
    if is_Union(sub): ...
    if is_Generic(sup): ...

So, what if both the subtype and supertype are type hints containing multiple generics subscripting multiple tuples subscripting multiple unions subscripting a dictionary subscripting a list (e.g., list[dict[Union[tuple[MuhGeneric, MuhOtherGeneric, ...], int], str]])? Those are hardly worst-case type hints, either.

Since there are a countably infinite number of possible type hints, there's an even larger number of 2-combinations (subtype, supertype) for any arbitrary type hint subtype being compared against any arbitrary type hint supertype. Ergo, there is some 2-combination of type hints that cannot possibly be handled by a linear chain of if conditionals. Indeed, the binomial coefficient tells us that the number of possible 2-combinations between n type hints grows explosively as:

(n 2) = "n pick 2" = n!/[2(n-2)!]

This is why C++ popularized operator overloading, right? By overloading the <= comparator within four C++ classes A, B, C, and D in a compatible manner, any two instances of those four classes can be implicitly compared without having to explicitly code a linear chain of if conditionals. That's good! Explicitly coding a linear chain of if conditionals rapidly becomes impossible in practice, because combinatorial explosion makes my brain burn.

What to Do, Then?

What to do is what we always do in Computer Science when confronted with a seemingly insurmountable, intractable, and infeasible mountain of pain: we transform that complex problem we cannot solve into a simpler problem we have already solved.

In this case, Python has already solved a simpler problem: deciding whether one type is a subclass of another (i.e., the issubclass(A, B) relation). If we can transform our complex is_subtype(A, B) relation we cannot readily solve into the simpler issubclass(A, B) relation that Python has already solved, then our job is done before it even began. Can we do this?

Yes, we can. you knew that was coming B-b-but... how can we? "Simple." The beartype.is_subtype(subtype, supertype) tester function should probably:

  1. Dynamically create one in-memory class for each inclusive superclass of each nested type hint of each passed type hint, ultimately producing two internal type hierarchies: a type hierarchy SubType representing the passed subtype type hint and yet another type hierarchy SuperType representing the passed supertype type hint. This operation has O(jk) time complexity (I think?), where:
    • j is the maximum number of superclasses of any nested type hint, equivalent to the length of the method resolution order (MRO) for that hint (i.e., j = len(type_hint.__mro__)).
    • k is the maximum number of nested type hints of each passed type hint.
  2. Return the result of calling issubclass(SubType, SuperType).

Obviously, 2. is a trivial one-liner. That just leaves 1., which is non-trivial but tractable. Consider this concrete example:

>>> from beartype import is_subtype
>>> from beartype.typing import list, Sequence, Union

# User-defined "int" subclass, just 'cause.
>>> class MuhInt(int): pass

>>> subtype = list[MuhInt]
>>> supertype = Union[Sequence[int], str]
>>> is_subtype(subtype, supertype)
True

Internally, beartype.is_subtype() will create two type hierarchies: one representing the passed subtype and the other representing the passed supertype. For this concrete example, these type hierarchies might resemble:

     # v--- beartype-specific fast caching protocols for the win
from beartype.typing import Protocol

# ------[ TYPE HIERARCHY FOR: list[MuhInt] ]------
# Type representing the type hint "list[object]".
class ListOfObject(list, Protocol):
    # Apparently, we have to explicitly declare this
    # dunder method or Python obliquely complains:
    #     TypeError: Protocols with non-method members don't support issubclass()
    def __hash__(self): pass

# Type representing the type hint "list[int]".
class ListOfInt(ListOfObject, Protocol):
    def has_int(): pass

# Type representing the type hint "list[MuhInt]".
class ListOfMuhInt(ListOfInt, Protocol):
    def has_MuhInt(): pass

# ------[ TYPE HIERARCHY FOR: Union[Sequence[int], str] ]------
# Type representing the type hint "Sequence[object]".
from collections.abc import Sequence
class SequenceOfObject(Sequence, Protocol): pass

# Type representing the type hint "Sequence[int]".
class SequenceOfInt(SequenceOfObject, Protocol):
    def has_int(): pass

# Type representing the type hint "Union[Sequence[int], str]".
UnionOfSequenceOfIntAndStr = SequenceOfInt | str
         # ^--- check dat dark PEP 604 magic out, bro

We can both agree there's a tractable algorithm for producing those two type hierarchies, right? Right! You're darn right! Indeed, note that Python itself already provides an out-of-the-box comparison between the builtin list type and the collections.abc.Sequence ABC:

>>> from collections.abc import Sequence
>>> issubclass(list, Sequence)
True

Because both the ListOfMuhInt and SequenceOfInt protocols declare the same has_int() method, the former is necessarily a subclass of the latter under PEP 544-style structural subtyping. 😮

We can also then agree that we've transformed our complex is_subtype(A, B) relation we cannot readily solve into the simpler issubclass(A, B) relation that Python has already solved. Altogether, this means that:

# This non-trivial function call copied from above...
>>> is_subtype(list[MuhInt], Union[Sequence[int], str])
# ...is now trivially reducible to this obvious function call
# leveraging the two type hierarchies we generated above.
>>> issubclass(ListOfMuhInt, UnionOfSequenceOfIntAndStr)
True

I've tested that. It actually works. Shocked Pikachu face, arise!

oh boy

...Can't Be That Easy

...heh. Yeah. You just know something's gonna blow that trivial algorithm up hard. But until that happens, let's choose to believe in the miraculous healing power of Python's issubclass() builtin. 😁

@tlambert03
Copy link
Contributor

Dynamically create one in-memory class for each inclusive superclass of each nested type hint of each passed type hint

brilliant! 🎉
I'm so sorry to hear about your bunkhouse & cottage, but I'm really glad/appreciative that this problem is still percolating a bit in your brain.

I will definitely take your example here and tinker with it some more. I'll open a PR if I hit on something useful (though I suspect it will not be up to beartype rigor).

You just know something's gonna blow that trivial algorithm up hard.

Yep, I'm more than happy to take this in stages. Particularly for the application of detecting API changes, even if this works 50% of the time, it's still way better than nothing. If we just quietly fail when it breaks, we haven't done anything worse than the situation we're already in... which is no checking whatsoever 😂

thanks again!

@leycec
Copy link
Member Author

leycec commented Jun 2, 2022

I'll open a PR if I hit on something useful (though I suspect it will not be up to beartype rigor).

Wunderbar! Literally anything is useful, because we currently have nothing. Unlike various other GitHub repositories that shall remain nameless, I've committed to merging every PR that passes tests in a prompt and shamelessly desperate manner. I'm here for the community, because the community's here for me. 🤗

What now follows are mostly unreadable notes to myself, because I will forget this tomorrow. In fact, I've already forgotten everything. Thanks fer nuthin', short-term memory recall. </gulp>

Unions: They're No Longer Just for Dock Workers

Above, I slyly suggested that union type hints (e.g., Union[Sequence[int], str]) could just be coerced into PEP 644-style new union objects (e.g., SequenceOfInt | str). Technically, that works in the exact example I gave. Pragmatically, I just got lucky. That doesn't work in general. PEP 644-style new union objects can't be subclassed, which means you can't generalize that coercion to embedded unions (e.g., list[Union[Sequence[int], str]]):

>>> class StrOrInt(str | int): pass
TypeError: cannot create 'types.UnionType' instances

That means we'll need to do something magical to support embedded unions. Since "magical" is synonymous with "pagan metaclass trickery" in Python, that means we'll need to define a new beartype.typing._typingpep544._CachingProtocolMeta.__subclasscheck__() dunder method handling embedded unions. Since the beartype.typing._typingpep544 submodule is already an incoherent nightmare despite the collective best efforts of the bear bros, that means...

This may never happen. Still, I hope with an ageless, burning hunger that someone who is not me tries to do something about this. They'll probably crash and burn, true – but at least they'll have tried.

So, Impossible?

Probably, yes. This is a darkly lit road that none dare tread. And I was born and raised in Los Angeles. I know everything about darkly lit roads that none dare tread.

@tlambert03
Copy link
Contributor

tlambert03 commented Jun 2, 2022

The source of mypy.subtypes.is_subtype will almost certainly be of relevance here as well.

Outside of difficulties of using mypy directly (i.e. outside of cli usage) the main reason it can't be used directly is that it accepts mypy.types.Type objects as arguments... I don't know enough about the internals of mypy to figure out how to construct one of those Type objects outside of mypy on the cli, but that might also be worth looking into (note to self)

@leycec
Copy link
Member Author

leycec commented Jun 3, 2022

lolbro. I did this, because Harvard Medical School deserves this best. Srsly, tho. Your feature request inspired me hard while clearing yet more tree wreckage from our based hugelkultur permaculture raised garden bed. Ergo, I did this for humanity and Harvard:

# In the existing "beartype.roar._roarexc" submodule:
class BeartypeMathException(BeartypeException):
    '''
    Abstract base class of all **beartype math exceptions.**

    Instances of subclasses of this exception are raised at call time from the
    callables and classes published by the :func:`beartype.math` subpackage.
    '''

    pass

# In a new "beartype.math.__init__" submodule:
from beartype.math._mathcls import TypeHint

# In a new "beartype.math._mathcls" submodule:
from beartype.roar import BeartypeMathException
from beartype.typing import (
    Iterable,
)
from beartype._data.hint.pep.sign.datapepsigns import (
    HintSignAbstractSet,
    HintSignAsyncContextManager,
    HintSignAsyncIterable,
    HintSignAsyncIterator,
    HintSignAwaitable,
    HintSignByteString,
    HintSignCollection,
    HintSignContainer,
    HintSignContextManager,
    HintSignCounter,
    HintSignDeque,
    HintSignFrozenSet,
    HintSignItemsView,
    HintSignIterable,
    HintSignIterator,
    HintSignKeysView,
    HintSignList,
    HintSignMatch,
    HintSignMutableSequence,
    HintSignMutableSet,
    HintSignPattern,
    HintSignSequence,
    HintSignSet,
    HintSignType,
    HintSignValuesView,
)
from beartype._data.hint.pep.sign.datapepsignset import (
    HINT_SIGNS_UNION,
)
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.hint.pep.utilpepget import (
    get_hint_pep_args,
    get_hint_pep_sign,
)
from collections.abc import Collection as CollectionABC
from functools import total_ordering

#FIXME: Unit test us up, please!
@total_ordering
class TypeHint(CollectionABC):
    '''
    Abstract base class (ABC) of all **totally ordered type hint** (i.e.,
    high-level object encapsulating a low-level type hint augmented with all
    rich comparison ordering methods).

    Instances of this class are totally ordered with respect to one another.
    Equivalently, instances of this class support all binary comparators (i.e.,
    ``==``, ``!=``, ``<``, ``<=``, ``>``, and ``>=``) according such that for
    any three instances ``a``, ``b`, and ``c`` of this class:

    * ``a ≤ a`` (i.e., reflexivity).
    * If ``a ≤ b`` and ``b ≤ c``, then ``a ≤ c`` (i.e., transitivity).
    * If ``a ≤ b`` and ``b ≤ a``, then ``a == b`` (i.e., antisymmetry).
    * Either ``a ≤ b`` or ``b ≤ a`` (i.e., totality).

    Instances of this class are thus usable in algorithms and data structures
    requiring a total ordering across their input.
    '''

    def __new__(cls, hint: object) -> 'TypeHint':
        '''
        Factory constructor magically instantiating and returning an instance of
        the private concrete subclass of this public abstract base class (ABC)
        appropriate for handling the passed low-level unordered type hint.

        Parameters
        ----------
        hint : object
            Lower-level unordered type hint to be encapsulated by this
            higher-level totally ordered type hint.

        Returns
        ----------
        TypeHint
           Higher-level totally ordered type hint encapsulating that hint.

        Raises
        ----------
        BeartypeMathException
            If this class does *not* currently support the passed hint.
        BeartypeDecorHintPepSignException
            If the passed hint is *not* actually a PEP-compliant type hint.
        '''

        # Sign uniquely identifying this hint if any *OR* raise an exception
        # (i.e., if this hint is *NOT* actually a PEP-compliant type hint).
        hint_sign = get_hint_pep_sign(hint)

        # Private concrete subclass of this ABC handling this hint if any *OR*
        # "None" otherwise (i.e., if no such subclass has been authored yet).
        TypeHintSubclass = _HINT_SIGN_TO_TypeHint.get(hint_sign)

        # If this hint appears to be currently unsupported...
        if TypeHintSubclass is None:
            # If this hint is a type, defer to the subclass handling types.
            if isinstance(hint, type):
                TypeHintSubclass = _TypeHintClass
            # Else, this hint is *NOT* a type. Ergo, this hint is unsupported.
            # In this case, raise an exception.
            else:
                raise BeartypeMathException(
                    f'Type hint {repr(hint)} currently unsupported by '
                    f'class "beartype.math.TypeHint".'
                )
        # Else, this hint is supported.

        # Return this subclass.
        return super().__new__(TypeHintSubclass)


    def __init__(self, hint: object) -> None:
        '''
        Initialize this high-level totally ordered type hint against the passed
        low-level unordered type hint.

        Parameters
        ----------
        hint : object
            Lower-level unordered type hint to be encapsulated by this
            higher-level totally ordered type hint.
        '''

        # Classify all passed parameters. Note that this type hint is guaranteed
        # to be a type hint by validation performed by the __new__() method.
        self._hint = hint

        # Tuple of all low-level unordered child type hints of this hint.
        self._hints_child_unordered = get_hint_pep_args(hint)

        # Tuple of all high-level totally ordered child type hints of this hint.
        self._hints_child_ordered = tuple(
            TypeHint(hint_child_unordered)
            for hint_child_unordered in self._hints_child_unordered
        )


    def __len__(self) -> int:
        return len(self._hints_child_ordered)


    def __iter__(self) -> Iterable['TypeHint']:
        '''
        Immutable iterable of all **children** (i.e., high-level totally ordered
        type hints encapsulating all low-level unordered child type hints
        subscripting (indexing) the low-level unordered parent type hint
        encapsulated by this high-level totally ordered parent type hint) of
        this totally ordered parent type hint.
        '''

        return self._hints_child_ordered


    #FIXME: Implement us up, please! The implementation should probably resemble
    #that of the __le__() method defined below. *phew*
    #FIXME: Docstring us up, please!
    @abstractmethod
    def __eq__(self, other: 'TypeHint') -> bool: pass

    @callable_cached
    def __le__(self, other: 'TypeHint') -> bool:
        '''
        ``True`` only if this totally ordered type hint is **compatible** with
        the passed totally ordered type hint, where compatibility implies that
        the unordered type hint encapsulated by this totally ordered type hint
        may be losslessly replaced by the unordered type hint encapsulated by
        the parent totally ordered type hint *without* breaking backward
        compatibility in APIs annotated by the former.

        This method is memoized and thus enjoys ``O(1)`` amortized worst-case
        time complexity across all calls to this method.
        '''

        # If the passed object is *NOT* a totally ordered type hint, raise an
        # exception.
        _die_unless_TypeHint(other)
        # Else, that object is a totally ordered type hint.

        # For each branch of the passed union if that hint is a union *OR* that
        # hint as is otherwise...
        for branch in other.branches():
            # If this hint is compatible with that branch, return true.
            if self._is_le_branch(branch):
                return True
            # Else, this hint is incompatible with that branch. In this case,
            # silently continue to the next branch.

        # Else, this hint is incompatible with that hint.
        return False



    @property
    def branches(self) -> Iterable['TypeHint']:
        '''
        Immutable iterable of all **branches** (i.e., high-level totally ordered
        type hints encapsulating all low-level unordered child type hints
        subscripting (indexing) the low-level unordered parent type hint
        encapsulated by this high-level totally ordered parent type hint if this
        is a union (and thus an instance of the :class:`_TypeHintUnion`
        subclass) *or* the 1-tuple containing only this instance itself
        otherwise) of this totally ordered parent type hint.

        This property enables the child type hints of :pep:`484`- and
        :pep:`604`-compliant unions (e.g., :attr:`typing.Union`,
        :attr:`typing.Optional`, and ``|``-delimited type objects) to be handled
        transparently *without* special cases in subclass implementations.
        '''

        # Default to returning the 1-tuple containing only this instance, as
        # *ALL* subclasses except "_HintTypeUnion" require this default.
        return (self,)


    @abstractmethod
    def _is_le_branch(self, branch: 'TypeHint') -> bool:
        '''
        ``True`` only if this totally ordered type hint is **compatible** with
        the passed branch of another totally ordered type hint passed to the
        parent call of the :meth:`__le__` dunder method.

        See Also
        ----------
        :meth:`__le__`
            Further details.
        '''

        pass


class _TypeHintClass(TypeHint):
    '''
    **Totally ordered class type hint** (i.e., high-level object encapsulating a
    low-level PEP-compliant type hint that is, in fact, a simple class).
    '''

    #FIXME: Define __eq__() too, please!

    def _is_le_branch(self, branch: TypeHint) -> bool:

        # Return true only if...
        return (
            # That branch is also a totally ordered class type hint *AND*...
            isinstance(branch, _TypeHintClass) and
            issubclass(
                # The low-level unordered type hint encapsulated by this
                # high-level totally ordered type hint is a subclass of...
                self._hint, 
                # The low-level unordered type hint encapsulated by that
                # branch...
                branch._hint
            )
        )


class _TypeHintSubscripted(TypeHint):
    '''
    **Totally ordered subscripted type hint** (i.e., high-level object
    encapsulating a low-level parent type hint subscripted (indexed) by one or
    more equally low-level children type hints).

    Attributes
    ----------
    _hints_child_unordered : tuple[object]
        Tuple of all low-level unordered children type hints of the low-level
        unordered parent type hint passed to the :meth:`__init__` method.
    _hints_child_ordered : tuple[TypeHint]
        Tuple of all high-level totally ordered children type hints of this
        high-level totally ordered parent type hint.
    '''

    def __init__(self, *args, **kwargs) -> None:
        '''
        Initialize this high-level totally ordered subscripted type hint against
        the passed low-level unordered subscripted type hint.

        Parameters
        ----------
        All passed parameters are passed as is to the superclass
        :meth:`TypeHint.__init__` method.
        '''

        # Initialize our superclass with all passed parameters.
        super().__init__(*args, **kwargs)

        #FIXME: Perform additional validation here, please. Notably, raise an
        #exception if this hint is subscripted by *NO* child type hints.
        # Tuple of all low-level unordered child type hints of this hint.
        self._hints_child_unordered = get_hint_pep_args(hint)

        # Tuple of all high-level totally ordered child type hints of this hint.
        self._hints_child_ordered = tuple(
            TypeHint(hint_child_unordered)
            for hint_child_unordered in self._hints_child_unordered
        )


class _TypeHintOriginIsinstanceableArgs1(_TypeHintSubscripted):
    '''
    **Totally ordered single-argument isinstanceable type hint** (i.e.,
    high-level object encapsulating a low-level PEP-compliant type hint
    subscriptable by only one child type hint originating from an
    isinstanceable class such that *all* objects satisfying that hint are
    instances of that class).
    '''

    #FIXME: Define __eq__() too, please!

    def _is_le_branch(self, branch: TypeHint) -> bool:

        # Return true only if...
        return (
            # That branch is also a totally ordered single-argument
            # isinstanceable type hint *AND*...
            isinstance(branch, _TypeHintOriginIsinstanceableArgs1) and
            issubclass(
                # The low-level unordered type hint encapsulated by this
                # high-level totally ordered type hint is a subclass of...
                self._hint,
                # The low-level unordered type hint encapsulated by that
                # branch...
                branch._hint
            ) and
            # The high-level totally ordered child type hint subscripted by this
            # high-level totally ordered parent type hint is "compatible" with
            # the high-level totally ordered child type hint subscripted by that
            # high-level totally ordered parent type hint.
            self._hints_child_unordered[0] <= branch._hints_child_unordered[0]
        )


class _TypeHintUnion(_TypeHintSubscripted):
    '''
    **Totally ordered union type hint** (i.e., high-level object encapsulating a
    low-level PEP-compliant union type hint, including both :pep:`484`-compliant
    :attr:`typing.Union` and :attr:`typing.Optional` unions *and*
    :pep:`604`-compliant ``|``-delimited type unions).
    '''

    #FIXME: Define __eq__() too, please!

    def branches(self) -> Iterable[TypeHint]:
        return self._hints_child_ordered

    @callable_cached
    def __le__(self, other: TypeHint) -> bool:

        # If the passed object is *NOT* a totally ordered type hint, raise an
        # exception.
        _die_unless_TypeHint(other)

        # If that hint is *NOT* a totally ordered union type hint, return false.
        if not isinstance(other, _TypeHintUnion):
            return False
        # Else, that hint is a totally ordered union type hint.

        #FIXME: O(n^2) complexity ain't that great. Perhaps that's unavoidable
        #here, though? Contemplate optimizations, please.

        # For each branch of this union...
        for self_branch in self.branches():
            # For each branch of that union...
            for other_branch in other.branches():
                # If this branch is compatible with that branch, return true.
                if self_branch <= other_branch:
                    return True

        # Else, this hint is incompatible with that hint.
        return False


    def _is_le_branch(self, branch: TypeHint) -> bool:

        #FIXME: Is this right? I have no idea. My brain hurts. The API could
        #probably be cleaned up a bit by:
        #* Shifting the TypeHint.__le__() method *IMPLEMENTATION* into
        #  "_TypeHintSubscripted".
        #* Decorating the TypeHint.__le__() method with @abstractmethod.
        #* Shifting the TypeHint._is_le_branch() method into
        #  "_TypeHintSubscripted".
        raise NotImplementedError('_TypeHintUnion._is_le_branch() unsupported.')


def _die_unless_TypeHint(obj: object) -> None:
    '''
    Raise an exception unless the passed object is a **totally ordered type
    hint** (i.e., :class:`TypeHint` instance).

    Parameters
    ----------
    obj : object
        Arbitrary object to be validated.

    Raises
    ----------
    BeartypeMathException
        If this object is *not* a totally ordered type hint.
    '''

    # If this object is *NOT* a totally ordered type hint, raise an exception.
    if not isinstance(obj, TypeHint):
        raise BeartypeMathException(
            f'{repr(obj)} not totally ordered type hint
            f'(i.e., "beartype.math.TypeHint" instance).'
        )


_HINT_SIGNS_ORIGIN_ISINSTANCEABLE_ARGS_1 = frozenset((
    HintSignAbstractSet,
    HintSignAsyncContextManager,
    HintSignAsyncIterable,
    HintSignAsyncIterator,
    HintSignAwaitable,
    HintSignByteString,
    HintSignCollection,
    HintSignContainer,
    HintSignContextManager,
    HintSignCounter,
    HintSignDeque,
    HintSignFrozenSet,
    HintSignItemsView,
    HintSignIterable,
    HintSignIterator,
    HintSignKeysView,
    HintSignList,
    HintSignMatch,
    HintSignMutableSequence,
    HintSignMutableSet,
    HintSignPattern,
    HintSignSequence,
    HintSignSet,
    HintSignType,
    HintSignValuesView,
))
'''
Frozen set of all signs uniquely identifying **single-argument PEP-compliant
type hints** (i.e., type hints subscriptable by only one child type hint)
originating from an **isinstanceable origin type** (i.e., isinstanceable class
such that *all* objects satisfying this hint are instances of this class).
'''


# Initialized below by the _init() function.
_HINT_SIGN_TO_TypeHint = {}
'''
Dictionary mapping from each sign uniquely identifying PEP-compliant type hints
to the :class:`TypeHint` subclass handling those hints.
'''


def _init() -> None:
    '''
    Initialize this submodule.
    '''

    # Fully initialize the "_HINT_SIGN_TO_TypeHint" dictionary declared above.
    for hint_sign in _HINT_SIGNS_ORIGIN_ISINSTANCEABLE_ARGS_1:
        _HINT_SIGN_TO_TypeHint[hint_sign] = _TypeHintOriginIsinstanceableArgs1
    for hint_sign in HINT_SIGNS_UNION:
        _HINT_SIGN_TO_TypeHint[hint_sign] = _TypeHintUnion


# Initialize this submodule.
_init()

Cogito ergo sum. Completely untested – but confident of worky. Humanity, you're welcome!

@tlambert03: Would you mind doctoring that up a bit, sprinkling in a few unit tests, and submitting a mostly working PR? If so, we can get this typing party started. If not, I'll still eventually get this typing party started. But your brave assistance would be welcome beyond all belief.

@leycec leycec changed the title [Feature Request] beartype.is_subtypes() API for deciding type hint A ⊆ B "compatibility" subset relations [Feature Request] New beartype.math API for performing type hint arithmetic Jun 3, 2022
@tlambert03
Copy link
Contributor

holy shit man.

I can absolutely run with this and submit a PR with tests. This start would have taken me a long time! Still need to wrap my head around it, but this gives me some golden honey to work with 🍯

Ergo, I did this for humanity and Harvard:

I can't speak for Harvard, but all of humanity thanks you ;)
more soon!

@leycec
Copy link
Member Author

leycec commented Jun 4, 2022

Last-minute edit for great glory. The ABC design above was a bit ad-hoc, which always happens when you're blasting out 500 lines of spaghetti after a lukewarm bath. Allow me to politely tighten that up for the crowd following along at home:

# In the "beartype.roar._roarexc" submodule:
class BeartypeMathException(BeartypeException):
    '''
    Abstract base class of all **beartype math exceptions.**

    Instances of subclasses of this exception are raised at call time from the
    callables and classes published by the :func:`beartype.math` subpackage.
    '''

    pass

# In the "beartype.math.__init__" submodule:
from beartype.math._mathcls import TypeHint

# In the "beartype.math._mathcls" submodule:
from abc import ABC, abstractmethod
from beartype.roar import BeartypeMathException
from beartype.typing import (
    Iterable,
)
from beartype._data.hint.pep.sign.datapepsigns import (
    HintSignAbstractSet,
    HintSignAsyncContextManager,
    HintSignAsyncIterable,
    HintSignAsyncIterator,
    HintSignAwaitable,
    HintSignByteString,
    HintSignCollection,
    HintSignContainer,
    HintSignContextManager,
    HintSignCounter,
    HintSignDeque,
    HintSignFrozenSet,
    HintSignItemsView,
    HintSignIterable,
    HintSignIterator,
    HintSignKeysView,
    HintSignList,
    HintSignMatch,
    HintSignMutableSequence,
    HintSignMutableSet,
    HintSignPattern,
    HintSignSequence,
    HintSignSet,
    HintSignType,
    HintSignValuesView,
)
from beartype._data.hint.pep.sign.datapepsignset import (
    HINT_SIGNS_UNION,
)
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.hint.pep.utilpepget import (
    get_hint_pep_args,
    get_hint_pep_origin,
    get_hint_pep_sign,
)
from functools import total_ordering

#FIXME: Unit test us up, please!
@total_ordering
class TypeHint(ABC):
    '''
    Abstract base class (ABC) of all **totally ordered type hint** (i.e.,
    high-level object encapsulating a low-level type hint augmented with all
    rich comparison ordering methods).

    Instances of this class are totally ordered with respect to one another.
    Equivalently, instances of this class support all binary comparators (i.e.,
    ``==``, ``!=``, ``<``, ``<=``, ``>``, and ``>=``) according such that for
    any three instances ``a``, ``b`, and ``c`` of this class:

    * ``a ≤ a`` (i.e., reflexivity).
    * If ``a ≤ b`` and ``b ≤ c``, then ``a ≤ c`` (i.e., transitivity).
    * If ``a ≤ b`` and ``b ≤ a``, then ``a == b`` (i.e., antisymmetry).
    * Either ``a ≤ b`` or ``b ≤ a`` (i.e., totality).

    Instances of this class are thus usable in algorithms and data structures
    requiring a total ordering across their input.

    Attributes
    ----------
    _hints_child_unordered : tuple[object]
        Tuple of all low-level unordered children type hints of the low-level
        unordered parent type hint passed to the :meth:`__init__` method.
    _hints_child_ordered : tuple[TypeHint]
        Tuple of all high-level totally ordered children type hints of this
        high-level totally ordered parent type hint.
    '''

    def __new__(cls, hint: object) -> 'TypeHint':
        '''
        Factory constructor magically instantiating and returning an instance of
        the private concrete subclass of this public abstract base class (ABC)
        appropriate for handling the passed low-level unordered type hint.

        Parameters
        ----------
        hint : object
            Lower-level unordered type hint to be encapsulated by this
            higher-level totally ordered type hint.

        Returns
        ----------
        TypeHint
           Higher-level totally ordered type hint encapsulating that hint.

        Raises
        ----------
        BeartypeMathException
            If this class does *not* currently support the passed hint.
        BeartypeDecorHintPepSignException
            If the passed hint is *not* actually a PEP-compliant type hint.
        '''

        # Sign uniquely identifying this hint if any *OR* raise an exception
        # (i.e., if this hint is *NOT* actually a PEP-compliant type hint).
        hint_sign = get_hint_pep_sign(hint)

        # Private concrete subclass of this ABC handling this hint if any *OR*
        # "None" otherwise (i.e., if no such subclass has been authored yet).
        TypeHintSubclass = _HINT_SIGN_TO_TypeHint.get(hint_sign)

        # If this hint appears to be currently unsupported...
        if TypeHintSubclass is None:
            # If this hint is a type, defer to the subclass handling types.
            if isinstance(hint, type):
                TypeHintSubclass = _TypeHintClass
            # Else, this hint is *NOT* a type. Ergo, this hint is unsupported.
            # In this case, raise an exception.
            else:
                raise BeartypeMathException(
                    f'Type hint {repr(hint)} currently unsupported by '
                    f'class "beartype.math.TypeHint".'
                )
        # Else, this hint is supported.

        # Return this subclass.
        return super().__new__(TypeHintSubclass)


    def __init__(self, hint: object) -> None:
        '''
        Initialize this high-level totally ordered type hint against the passed
        low-level unordered type hint.

        Parameters
        ----------
        hint : object
            Lower-level unordered type hint to be encapsulated by this
            higher-level totally ordered type hint.
        '''

        # Classify all passed parameters. Note that this type hint is guaranteed
        # to be a type hint by validation performed by the __new__() method.
        self._hint = hint

        #FIXME: Insufficient. This should also handle unsubscripted type hints
        #(e.g., "typing.List", "typing.AbstractSet"). PEP 484 specifies that such
        #hints are semantically equivalent to the equivalent type hints
        #subscripted by the "object" superclass. Ergo, we should expand here:
        #* "typing.List" as if it were "typing.List[object]".
        #* "typing.AbstractSet" as if it were "typing.AbstractSet[object]".

        # Tuple of all low-level unordered child type hints of this hint.
        self._hints_child_unordered = get_hint_pep_args(hint)

        # Tuple of all high-level totally ordered child type hints of this hint.
        self._hints_child_ordered = tuple(
            TypeHint(hint_child_unordered)
            for hint_child_unordered in self._hints_child_unordered
        )


    #FIXME: Implement us up, please! The implementation should probably resemble
    #that of the concrete __le__() methods defined below. *phew*
    #FIXME: Docstring us up, please!
    @abstractmethod
    def __eq__(self, other: 'TypeHint') -> bool: pass

    @abstractmethod
    def __le__(self, other: 'TypeHint') -> bool:
        '''
        ``True`` only if this totally ordered type hint is **compatible** with
        the passed totally ordered type hint, where compatibility implies that
        the unordered type hint encapsulated by this totally ordered type hint
        may be losslessly replaced by the unordered type hint encapsulated by
        the parent totally ordered type hint *without* breaking backward
        compatibility in APIs annotated by the former.

        This method is memoized and thus enjoys ``O(1)`` amortized worst-case
        time complexity across all calls to this method.
        '''

        pass


    @property
    @abstractmethod
    def branches(self) -> Iterable['TypeHint']:
        '''
        Immutable iterable of all **branches** (i.e., high-level totally ordered
        type hints encapsulating all low-level unordered child type hints
        subscripting (indexing) the low-level unordered parent type hint
        encapsulated by this high-level totally ordered parent type hint if this
        is a union (and thus an instance of the :class:`_TypeHintUnion`
        subclass) *or* the 1-tuple containing only this instance itself
        otherwise) of this totally ordered parent type hint.

        This property enables the child type hints of :pep:`484`- and
        :pep:`604`-compliant unions (e.g., :attr:`typing.Union`,
        :attr:`typing.Optional`, and ``|``-delimited type objects) to be handled
        transparently *without* special cases in subclass implementations.
        '''

        pass


class _TypeHintNonunion(TypeHint):
    '''
    **Totally ordered non-union type hint** (i.e., high-level object
    encapsulating any low-level parent type hint *other* than a :pep:`484`- or
    :pep:`604`-compliant type hint).
    '''

    @property
    def branches(self) -> Iterable[TypeHint]:

        # Default to returning the 1-tuple containing only this instance, as
        # *ALL* subclasses except "_HintTypeUnion" require this default.
        return (self,)


    @callable_cached
    def __le__(self, other: TypeHint) -> bool:
        '''
        ``True`` only if this totally ordered type hint is **compatible** with
        the passed totally ordered type hint, where compatibility implies that
        the unordered type hint encapsulated by this totally ordered type hint
        may be losslessly replaced by the unordered type hint encapsulated by
        the parent totally ordered type hint *without* breaking backward
        compatibility in APIs annotated by the former.

        This method is memoized and thus enjoys ``O(1)`` amortized worst-case
        time complexity across all calls to this method.
        '''

        # If the passed object is *NOT* a totally ordered type hint, raise an
        # exception.
        _die_unless_TypeHint(other)
        # Else, that object is a totally ordered type hint.

        # For each branch of the passed union if that hint is a union *OR* that
        # hint as is otherwise...
        for branch in other.branches:
            # If this hint is compatible with that branch, return true.
            if self._is_le_branch(branch):
                return True
            # Else, this hint is incompatible with that branch. In this case,
            # silently continue to the next branch.

        # Else, this hint is incompatible with that hint.
        return False


    @abstractmethod
    def _is_le_branch(self, branch: TypeHint) -> bool:
        '''
        ``True`` only if this totally ordered type hint is **compatible** with
        the passed branch of another totally ordered type hint passed to the
        parent call of the :meth:`__le__` dunder method.

        See Also
        ----------
        :meth:`__le__`
            Further details.
        '''

        pass


class _TypeHintClass(_TypeHintNonunion):
    '''
    **Totally ordered class type hint** (i.e., high-level object encapsulating a
    low-level PEP-compliant type hint that is, in fact, a simple class).
    '''

    #FIXME: Define __eq__() too, please!

    def _is_le_branch(self, branch: TypeHint) -> bool:

        #FIXME: Insufficient. This should also support comparison of a
        #class against a union (i.e., "if isinstance(branch, _TypeHintUnion)").
        #We leave this as an exercise for the perspicacious reader. :p
        # Return true only if...
        return (
            # That branch is also a totally ordered class type hint *AND*...
            isinstance(branch, _TypeHintClass) and
            issubclass(
                # The low-level unordered type hint encapsulated by this
                # high-level totally ordered type hint is a subclass of...
                self._hint,
                # The low-level unordered type hint encapsulated by that
                # branch...
                branch._hint
            )
        )


class _TypeHintOriginIsinstanceableArgs1(_TypeHintNonunion):
    '''
    **Totally ordered single-argument isinstanceable type hint** (i.e.,
    high-level object encapsulating a low-level PEP-compliant type hint
    subscriptable by only one child type hint originating from an
    isinstanceable class such that *all* objects satisfying that hint are
    instances of that class).
    '''

    #FIXME: Define __eq__() too, please!

    def _is_le_branch(self, branch: TypeHint) -> bool:

        # Return true only if...
        return (
            # That branch is also a totally ordered single-argument
            # isinstanceable type hint *AND*...
            isinstance(branch, _TypeHintOriginIsinstanceableArgs1) and
            issubclass(
                # The issubclassable class underlying the low-level
                # unordered type hint encapsulated by this
                # high-level totally ordered type hint is a subclass of...
                get_hint_pep_origin(self._hint),
                # The issubclassable class underlying the low-level
                # unordered type hint encapsulated by that branch...
                get_hint_pep_origin(branch._hint)
            ) and
            # The high-level totally ordered child type hint subscripted by this
            # high-level totally ordered parent type hint is "compatible" with
            # the high-level totally ordered child type hint subscripted by that
            # high-level totally ordered parent type hint.
            self._hints_child_ordered[0] <= branch._hints_child_ordered[0]
        )


class _TypeHintUnion(TypeHint):
    '''
    **Totally ordered union type hint** (i.e., high-level object encapsulating a
    low-level PEP-compliant union type hint, including both :pep:`484`-compliant
    :attr:`typing.Union` and :attr:`typing.Optional` unions *and*
    :pep:`604`-compliant ``|``-delimited type unions).
    '''

    #FIXME: Define __eq__() too, please!

    @property
    def branches(self) -> Iterable[TypeHint]:
        return self._hints_child_ordered


    @callable_cached
    def __le__(self, other: TypeHint) -> bool:

        # If the passed object is *NOT* a totally ordered type hint, raise an
        # exception.
        _die_unless_TypeHint(other)

        # If that hint is *NOT* a totally ordered union type hint, return false.
        if not isinstance(other, _TypeHintUnion):
            return False
        # Else, that hint is a totally ordered union type hint.

        #FIXME: O(n**2) complexity ain't that great. Perhaps that's unavoidable
        #here, though? Contemplate up a few optimizations, please.

        # For each branch of this union...
        for self_branch in self.branches:
            # For each branch of that union...
            for other_branch in other.branches:
                # If this branch is compatible with that branch, return true.
                if self_branch <= other_branch:
                    return True

        # Else, this hint is incompatible with that hint.
        return False


def _die_unless_TypeHint(obj: object) -> None:
    '''
    Raise an exception unless the passed object is a **totally ordered type
    hint** (i.e., :class:`TypeHint` instance).

    Parameters
    ----------
    obj : object
        Arbitrary object to be validated.

    Raises
    ----------
    BeartypeMathException
        If this object is *not* a totally ordered type hint.
    '''

    # If this object is *NOT* a totally ordered type hint, raise an exception.
    if not isinstance(obj, TypeHint):
        raise BeartypeMathException(
            f'{repr(obj)} not totally ordered type hint '
            f'(i.e., "beartype.math.TypeHint" instance).'
        )


_HINT_SIGNS_ORIGIN_ISINSTANCEABLE_ARGS_1 = frozenset((
    HintSignAbstractSet,
    HintSignAsyncContextManager,
    HintSignAsyncIterable,
    HintSignAsyncIterator,
    HintSignAwaitable,
    HintSignByteString,
    HintSignCollection,
    HintSignContainer,
    HintSignContextManager,
    HintSignCounter,
    HintSignDeque,
    HintSignFrozenSet,
    HintSignItemsView,
    HintSignIterable,
    HintSignIterator,
    HintSignKeysView,
    HintSignList,
    HintSignMatch,
    HintSignMutableSequence,
    HintSignMutableSet,
    HintSignPattern,
    HintSignSequence,
    HintSignSet,
    HintSignType,
    HintSignValuesView,
))
'''
Frozen set of all signs uniquely identifying **single-argument PEP-compliant
type hints** (i.e., type hints subscriptable by only one child type hint)
originating from an **isinstanceable origin type** (i.e., isinstanceable class
such that *all* objects satisfying this hint are instances of this class).
'''


# Initialized below by the _init() function.
_HINT_SIGN_TO_TypeHint = {}
'''
Dictionary mapping from each sign uniquely identifying PEP-compliant type hints
to the :class:`TypeHint` subclass handling those hints.
'''


def _init() -> None:
    '''
    Initialize this submodule.
    '''

    # Fully initialize the "_HINT_SIGN_TO_TypeHint" dictionary declared above.
    for hint_sign in _HINT_SIGNS_ORIGIN_ISINSTANCEABLE_ARGS_1:
        _HINT_SIGN_TO_TypeHint[hint_sign] = _TypeHintOriginIsinstanceableArgs1
    for hint_sign in HINT_SIGNS_UNION:
        _HINT_SIGN_TO_TypeHint[hint_sign] = _TypeHintUnion


# Initialize this submodule.
_init()

Feelin' good about that. No more last-minute edits, I solemnly swear on my sacred honour as a GitHub techbro. Let the good typing times roll, everyone! 🎳

What We Actually Did Is This

Oh – and here's how to actually use that API. Currently, the above implementation explicitly supports:

  • PEP 604-style unions.
  • typing.Optional.
  • typing.Union.
  • typing.AbstractSet.
  • typing.AsyncContextManager.
  • typing.AsyncIterable.
  • typing.AsyncIterator.
  • typing.Awaitable.
  • typing.ByteString.
  • typing.Collection.
  • typing.Container.
  • typing.ContextManager.
  • typing.Counter.
  • typing.Deque.
  • typing.FrozenSet.
  • typing.ItemsView.
  • typing.Iterable.
  • typing.Iterator.
  • typing.KeysView.
  • typing.List.
  • typing.Match.
  • typing.MutableSequence.
  • typing.MutableSet.
  • typing.Pattern.
  • typing.Sequence.
  • typing.Set.
  • typing.Type.
  • typing.ValuesView.

Not too shabby for a first-draft off-the-cuff back-of-the-envelope hack job. Right? Right?

Okay. So, the public beartype.math.TypeHint class defines a total ordering over the set of all PEP-compliant type hints according to the relation of "type hint compatibility," which we just made up. Nobody's formalized that with a standard yet, but... let's pretend this is formally rigorous and that we actually know what we're doing. Here's how you'd use beartype.math.TypeHint to decide subtype compatibility for various example type hints:

>>> from beartype.math import TypeHint
>>> from beartype.typing import Sequence, Union
>>> TypeHint(list[str]) <= TypeHint(Sequence[str])
True
>>> TypeHint(list[str]) <= TypeHint(Union[Sequence[str], int])
True
>>> TypeHint(str | list | int) <= TypeHint(str | list | int | bool)
True
>>> TypeHint(Sequence[str]) <= TypeHint(list[str])
False

More to come... as you define it for us! Please do run with this if you find a spare weekend lying around anywhere. Type hint arithmetic is fun, everybody. 👨‍🏫

@tlambert03
Copy link
Contributor

awesome, thanks as always! Didn't see this before I started on #136, but will have a look at the diff and incorporate changes this weekend. The pattern seems to be working nicely! 👍

leycec pushed a commit that referenced this issue Jul 6, 2022
This commit by Harvard microscopist and general genius @tlambert03
defines a new public `beartype.math` subpackage for performing type
hint arithmetic, resolving issues #133 and #138 kindly also submitted
by @tlambert03. Specifically, this commit defines a:

* Public `beartype.math.TypeHint({type_hint})` class, enabling rich
  comparisons between pairs of arbitrary type hints. Altogether, this
  class implements a partial ordering over the countably infinite set
  of all type hints. Pedagogical excitement ensues.
* Public `beartype.math.is_subtype({type_hint_a}, {type_hint_b})`
  class, enabling @beartype users to decide whether any type hint
  is a **subtype** (i.e., narrower type hint) of any other type hint.

Thanks so much to @tlambert03 for his phenomenal work here.
(*Compelling compulsion of propulsive propellers!*)
@leycec
Copy link
Member Author

leycec commented Jul 6, 2022

Resolved by 7809ea2. Can't believe you did this, @tlambert03 – but you did. Now all peoples across Planet Earth hail and praise you.

@tlambert03
Copy link
Contributor

you're too generous @leycec 😂 it was your pattern too! it was a pleasure working with you, thanks for the help!

leycec added a commit that referenced this issue Sep 18, 2022
This minor release unleashes a major firestorm of support for **class
decoration,** **colourful exceptions,** **pyright + PyLance + VSCode,**
[PEP 484][PEP 484], [PEP 544][PEP 544], [PEP 561][PEP 561],
[PEP 563][PEP 563], [PEP 585][PEP 585], [PEP 604][PEP 604],
[PEP 612][PEP 612], and [PEP 647][PEP 647].

This minor release resolves a mammoth **29 issues** and merges **12 pull
requests.** Noteworthy changes include:

## Compatibility Improved

* **Class decoration.** The `@beartype` decorator now decorates both
  higher-level classes *and* lower-level callables (i.e., functions,
  methods), resolving feature request #152 kindly submitted by @posita
  the positively sublime. All possible edge cases are supported,
  including:
  * Classes defining methods decorated by builtin decorators: i.e.,
    * Class methods via `@classmethod`.
    * Static methods via `@staticmethod`.
    * Property getters, setters, and deleters via `@property`.
  * Arbitrarily deeply nested (i.e., inner) classes.
  * Arbitrarily deeply nested (i.e., inner) classes whose type hints are
    postponed under [PEP 563][PEP 563].
  Since this was surprisingly trivial, @leycec
  probably should have done this a few years ago. He didn't. This is why
  he laments into his oatmeal in late 2022.
* **[PEP 484][PEP 484]- and [PEP 585][PEP 585]-compliant nested
  generics.** @beartype now supports arbitrarily complex [PEP 484][PEP
  484]- and [PEP 585][PEP 585]-compliant inheritance trees subclassing
  non-trivial combinations of the `typing.Generic` superclass and other
  `typing` pseudo-superclasses, resolving issue #140 kindly submitted by
  @langfield (William Blake – yes, *that* William Blake). Notably,
  this release extricated our transitive visitation of the tree of all
  pseudo-superclasses of any PEP 484- and 585-compliant generic type
  hint (*...don't ask*) from its prior hidden sacred cave deep within
  the private `beartype._decor._code._pep._pephint` submodule into a new
  reusable `iter_hint_pep484585_generic_bases_unerased_tree()`
  generator, which is now believed to be the most fully-compliant
  algorithm for traversing generic inheritance trees at runtime. This
  cleanly resolved all lingering issues surrounding generics,
  dramatically reduced the likelihood of more issues surrounding
  generics, and streamlined the resolution of any more issues
  surrounding generics should they arise... *which they won't.*
  Generics: we have resoundingly beaten you. Stay down, please.
* **[PEP 544][PEP 544] compatibility.** @beartype now supports
  arbitrarily complex [PEP 544][PEP 544]-compliant inheritance trees
  subclassing non-trivial combinations of the `typing.Protocol` +
  `abc.ABC` superclasses, resolving #117 kindly submitted by
  too-entertaining pun master @twoertwein (Torsten Wörtwein).
  Notably, `@beartype` now:
  * Correctly detects non-protocols as non-protocols. Previously,
    @beartype erroneously detected a subset of non-protocols as
    PEP 544-compliant protocols. It's best not to ask why.
  * Ignores both the unsubscripted `beartype.typing.Protocol` superclass
    *and* parametrizations of that superclass by one or more type
    variables (e.g., `beartype.typing.Protocol[typing.TypeVar('T')]`) as
    semantically meaningless in accordance with similar treatment of the
    `typing.Protocol` superclass.
  * Permits caller-defined abstract protocols subclassing our caching
    `beartype.typing.Protocol` superclass to themselves be subclassed by
    one or more concrete subclasses. Previously, attempting to do so
    would raise non-human-readable exceptions from the `typing` module;
    now, doing so behaves as expected.
  * Relaxed our prior bad assumption that the second-to-last superclass
    of all generics – and thus protocols – is the `typing.Generic`
    superclass. That assumption *only* holds for standard generics and
    protocols; non-standard protocols subclassing non-`typing`
    superclasses (e.g., the `abc.ABC` superclass) *after* the list
    `typing` superclass in their method resolution order (MRO)
    flagrantly violate this assumption. Well, that's fine. We're fine
    with that. What's not fine about that? **Fine. This is fine.**
  * Avoids a circular import dependency. Previously, our caching
    `beartype.typing.Protocol` superclass leveraged the general-purpose
    `@beartype._util.cache.utilcachecall.callable_cached decorator` to
    memoize its subscription; however, since that decorator transitively
    imports from the `beartype.typing` subpackage, doing so induced a
    circular import dependency. To circumvent this, a new
    `@beartype.typing._typingcache.callable_cached_minimal` decorator
    implementing only the minimal subset of the full
    `@beartype._util.cache.utilcachecall.callable_cached` decorator has
    been defined; the `beartype.typing` subpackage now safely defers to
    this minimal variant for all its caching needs.
* **[PEP 563][PEP 563] compatibility.** @beartype now resolves [PEP
  563][PEP 563]-postponed **self-referential type hints** (i.e., type
  hints circularly referring to the class currently being decorated).
  **Caveat:** this support requires that external callers decorate the
  *class* being referred to (rather than the *method* doing the
  referring) by the `@beartype` decorator. For this and similar reasons,
  users are advised to begin refactoring their object-oriented codebases
  to decorate their *classes* rather than *methods* with `@beartype`.
* **[PEP 612][PEP 612] partial shallow compatibility.** @beartype now
  shallowly detects [PEP 612][PEP 612]-compliant `typing.ParamSpec`
  objects by internally associating such objects with our
  `beartype._data.hint.pep.sign.datapepsigns.HintSignParamSpec`
  singleton, enabling @beartype to portably introspect
  `Callable[typing.ParamSpec(...), ...]` type hints.
* **Static type-checking.** @beartype is now substantially more
  compliant with static type-checkers, including:
  * **Microsoft [pyright](https://github.com/microsoft/pyright) +
    [PyLance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance)
    + [VSCode](https://visualstudio.com).** @beartype now officially
    supports pyright, Microsoft's in-house static type-checker oddly
    implemented in pure-TypeScript, <sup>*gulp*</sup> resolving issues
    #126 and #127 kindly submitted by fellow Zelda aficionado @rbroderi.
    Specifically, this release resolves several hundred false warnings
    and errors issued by pyright against the @beartype codebase. It is,
    indeed, dangerous to go alone – but we did it anyway.
  * **mypy `beartype.typing.Protocol` compatibility.** The
    @beartype-specific `beartype.typing.Protocol` superclass implementing
    [PEP 544][PEP 544]-compliant fast caching protocols is now fully
    compatible with mypy, Python's official static type-checker.
    Specifically, `beartype.typing.Protocol` now circumvents:
    * python/mypy#11013 by explicitly annotating the type of its
      `__slots__` as `Any`.
    * python/mypy#9282 by explicitly setting the `typing.TypeVar()`
      `bounds` parameter to this superclass.
* **[PEP 647][PEP 647] compatibility.** @beartype now supports
  arbitrarily complex **[type
  narrowing](https://mypy.readthedocs.io/en/latest/type_narrowing.html)**
  in [PEP 647][PEP 647]-compliant static type-checkers (e.g., mypy,
  pyright), resolving issues #164 and #165 kindly submitted in parallel
  by foxy machine learning gurus @justinchuby (Justin Chuby) and @rsokl
  (Ryan Soklaski). Thanks to their earnest dedication, @beartype is now
  believed to be the most fully complete type narrower. Specifically,
  the return of both the `beartype.door.is_bearable()` function and
  corresponding `beartype.door.TypeHint.is_bearable()` method are now
  annotated by the [PEP 647][PEP 647]-compliant `typing.TypeGuard[...]`
  type hint under both Python ≥ 3.10 *and* Python < 3.10 when the
  optional third-party `typing_extensions` dependency is installed.
  Doing so substantially reduces false positives from static type
  checkers on downstream codebases deferring to these callables.
  Thanks so much for improving @beartype so much, @justinchuby and
  @rsokl!
* **`@{classmethod,staticmethod,property}` chaining.** The `@beartype`
  decorator now implicitly supports callables decorated by both
  `@beartype` *and* one of the builtin method decorators `@classmethod`,
  `@staticmethod`, or `@property` regardless of decoration order,
  resolving issue #80 kindly requested by @qiujiangkun (AKA, Type
  Genius-kun). Previously, `@beartype` explicitly raised an exception
  when ordered *after* one of those builtin method decorators. This
  releseae relaxes this constraint, enabling callers to list `@beartype`
  either before or after one of those builtin method decorators.
* **`beartype.vale.Is[...]` integration.** Functional validators (i.e.,
  `beartype.vale.Is[...]`) now integrate more cleanly with the remainder
  of the Python ecosystem, including:
  * **IPython.** Functional validators localized to a sufficiently
    intelligent REPL (e.g., IPython) that caches locally defined
    callables to the standard `linecache` module now raise
    human-readable errors on type-checking, resolving issue #123 kindly
    submitted by typing brain-child @braniii. Relatedly, @beartype now
    permissively accepts both physical on-disk files and dynamic
    in-memory fake files cached with `linecache` as the files defining
    an arbitrary callable.
  * **NumPy,** which publishes various **bool-like tester functions**
    (i.e., functions returning a non-`bool` object whose class defines
    at least one of the `__bool__()` or `__len__()` dunder methods and
    is thus implicitly convertible into a `bool`). Functional validators
    now support subscription by these functions, resolving issue #153
    kindly submitted by molecular luminary @braniii (Daniel Nagel).
    Specifically, @beartype now unconditionally wraps *all* tester
    callables subscripting (indexing) `beartype.vale.Is` with a new
    private `_is_valid_bool()` closure that (in order):
    1. Detects when those tester callables return bool-like objects.
    2. Coerces those objects into corresponding `bool` values.
    3. Returns those `bool` values instead.
* **Moar fake builtin types.**@beartype now detects all known **fake
  builtin types** (i.e., C-based types falsely advertising themselves as
  being builtin and thus *not* require explicit importation), succinctly
  resolving issue #158 kindly submitted by the decorous typing gentleman
  @langfield. Specifically, @beartype now recognizes instances of all of
  the following as fake builtin types:
  * `beartype.cave.AsyncCoroutineCType`.
  * `beartype.cave.AsyncGeneratorCType`.
  * `beartype.cave.CallableCodeObjectType`.
  * `beartype.cave.CallableFrameType`.
  * `beartype.cave.ClassDictType`.
  * `beartype.cave.ClassType`.
  * `beartype.cave.ClosureVarCellType`.
  * `beartype.cave.EllipsisType`.
  * `beartype.cave.ExceptionTracebackType`.
  * `beartype.cave.FunctionType`.
  * `beartype.cave.FunctionOrMethodCType`.
  * `beartype.cave.GeneratorCType`.
  * `beartype.cave.MethodBoundInstanceDunderCType`.
  * `beartype.cave.MethodBoundInstanceOrClassType`.
  * `beartype.cave.MethodDecoratorBuiltinTypes`.
  * `beartype.cave.MethodUnboundClassCType`.
  * `beartype.cave.MethodUnboundInstanceDunderCType`.
  * `beartype.cave.MethodUnboundInstanceNondunderCType`.
  * `beartype.cave.MethodUnboundPropertyNontrivialCExtensionType`.
  * `beartype.cave.MethodUnboundPropertyTrivialCExtensionType`.

## Compatibility Broken

* **Python 3.6.x support dropped.** This release unilaterally drops
  support for the Python 3.6.x series, which somnambulantly collided
  with its End-of-Life (EOL) a year ago and now constitutes a compelling
  security risk. Doing so substantially streamlines the codebase, whose
  support for Python 3.6.x required an unmaintainable writhing nest of
  wicked corner cases. We all now breathe a sigh of contentment in the
  temporary stillness of morning.
* **`beartype.cave` deprecation removals.** This release removes all
  deprecated third-party attributes from the `beartype.cave` submodule.
  The continued existence of these attributes substantially increased
  the cost of importing *anything* from our mostly undocumented
  `beartype.cave` submodule, rendering that submodule even less useful
  than it already is. Specifically, this release removes these
  previously deprecated attributes:
  * `beartype.cave.NumpyArrayType`.
  * `beartype.cave.NumpyScalarType`.
  * `beartype.cave.SequenceOrNumpyArrayTypes`.
  * `beartype.cave.SequenceMutableOrNumpyArrayTypes`.
  * `beartype.cave.SetuptoolsVersionTypes`.
  * `beartype.cave.VersionComparableTypes`.
  * `beartype.cave.VersionTypes`.

## Exceptions Improved

* **Colour** – the sensation formerly known as "color." @beartype now
  emits colourized type-checking violations (i.e.,
  `beartype.roar.BeartypeCallHintViolation` exceptions) raised by both
  `@beartype`-decorated callables *and* statement-level type-checkers
  (e.g., `beartype.door.die_if_unbearable()`,
  `beartype.door.TypeHint.die_if_unbearable()`), resolving issue #161
  kindly submitted by foxy machine learning expert @justinchuby (Justin
  Chu). When standard output is attached to an interactive terminal
  (TTY), ANSII-flavoured colours now syntactically highlight various
  substrings of those violations for improved visibility, readability,
  and debuggability. Since *all* actively maintained versions of Windows
  (i.e., Windows ≥ 10) now widely support ANSII escape sequences across
  both Microsoft-managed terminals (e.g., Windows Terminal) and
  Microsoft-managed Integrated Development Environments (IDEs) (e.g.,
  VSCode), this supports extends to Windows as well. The bad old days of
  non-standard behaviour are behind us all. Thanks *so* much to
  @justinchuby for his immense contribution to the righteous cause of
  eye-pleasing user experience (UX)!
* **Types disambiguated.** @beartype now explicitly disambiguates the
  types of parameters and returns that violate type-checking in
  exception messages raised by the `@beartype` decorator, resolving
  issue #124 kindly submitted by typing brain-child @braniii. Thus was
  justice restored to the QAverse.
* **Stack frame squelched.** @beartype now intentionally squelches
  (i.e., hides) the ignorable stack frame encapsulating the call to our
  private `beartype._decor._error.errormain.get_beartype_violation()`
  getter from the parent type-checking wrapper function generated by the
  :mod:`beartype.beartype` decorator, resolving issue #140 kindly
  submitted by @langfield (William Blake – yes, *that* William Blake).
  That stack frame only needlessly complicated visual inspection of
  type-checking violations in tracebacks – especially from testing
  frameworks like :mod:`pytest` that recapitulate the full definition of
  the `get_beartype_violation()` getter (including verbose docstring) in
  those tracebacks. Specifically, this release:
  * Renamed the poorly named `raise_pep_call_exception()` function to
    `get_beartype_violation()` for clarity.
  * Refactored `get_beartype_violation()` to return rather than raise
    `BeartypeCallHintViolation` exceptions (while still raising all
    other types of unexpected exceptions for robustness).
  * Refactored type-checking wrapper functions to directly raise the
    exception returned by calling `get_beartype_violation()`.
* **``None`` type.** The type of the ``None`` singleton is no longer
  erroneously labelled as a PEP 544-compliant protocol in type-checking
  violations. Let's pretend that never happened.
* **`beartype.abby.die_if_unbearable()` violations.** The
  `beartype.abby.die_if_unbearable()` validator function no longer
  raises non-human-readable exception messages prefixed by the
  unexpected substring `"@beartyped
  beartype.abby._abbytest._get_type_checker._die_if_unbearable()
  return"`. "Surely that never happened, @beartype!"

## Features Added

* **`beartype.door.** @beartype now provides a new public framework for
  introspecting, sorting, and type-checking type hints at runtime in
  constant time. N-n-now... hear me out here. @leycec came up with a
  ludicrous acronym and we're going to have to learn to live with it:
  the **D**ecidedly **O**bject-**O**rientedly **R**ecursive (DOOR) API.
  Or, `beartype.door` for short. Open the door to a whole new
  type-hinting world, everyone. `beartype.door` enables type hint
  arithmetic via an object-oriented type hint class hierarchy
  encapsulating the crude non-object-oriented type hint declarative API
  standardized by the :mod:`typing` module, resolving issues #133 and
  #138 kindly submitted by Harvard microscopist and general genius
  @tlambert03. The new `beartype.door` subpackage defines a public:
  * `TypeHint({type_hint})` superclass, enabling rich comparisons
    between pairs of arbitrary type hints. Altogether, this class
    implements a partial ordering over the countably infinite set of all
    type hints. Pedagogical excitement ensues. Instances of this class
    efficiently satisfy both the `collections.abc.Sequence` and
    `collections.abc.FrozenSet` abstract base classes (ABC) and thus
    behave just like tuples and frozen sets over child type hints.
    Public attributes defined by this class include:
    * A pair of `die_if_unbearable()` and `is_bearable()` runtime
      type-checking methods, analogous in behaviour to the existing
      `beartype.abby.die_if_unbearable()` and
      `beartype.abby.is_bearable()` runtime type-checking functions.
    * `TypeHint.is_bearable()`, currently implemented in terms of the
      procedural `beartype.abby.is_bearable()` tester.
    * An `is_ignorable` property evaluating to `True` only if the
      current type hint is semantically ignorable (e.g., `object`,
      `typing.Any`). There exist a countably infinite number of
      semantically ignorable type hints. The more you know, the less you
      want to read this changeset.
    * The equality comparison operator (e.g., `==`), enabling type hints
      to be compared according to semantic equivalence.
    * Rich comparison operators (e.g., `<=`, `>`), enabling type hints
      to be compared and sorted according to semantic narrowing.
    * A sane `__bool__()` dunder method, enabling type hint wrappers to
      be trivially evaluated as booleans according to the child type
      hints subscripting the wrapped type hints.
    * A sane `__len__()` dunder method, enabling type hint wrappers to
      be trivially sized according to the child type hints subscripting
      the wrapped type hints.
    * A sane `__contains__()` dunder method, enabling type hint wrappers
      to be tested for child type hint membership – just like builtin
      sets, frozen sets, and dictionaries.
    * A sane `__getindex__()` dunder method, enabling type hint wrappers
      to be subscripted by both positive and negative indices as well as
      slices of such indices – just like builtin tuples.
  * `beartype.door.AnnotatedTypeHint` subclass.
  * `beartype.door.CallableTypeHint` subclass.
  * `beartype.door.LiteralTypeHint` subclass.
  * `beartype.door.NewTypeTypeHint` subclass.
  * `beartype.door.TupleTypeHint` subclass.
  * `beartype.door.TypeVarTypeHint` subclass.
  * `beartype.door.UnionTypeHint` subclass.
  * `is_subtype({type_hint_a}, {type_hint_b})` function, enabling
    @beartype users to decide whether any type hint is a **subtype**
    (i.e., narrower type hint) of any other type hint.
  * `beartype.roar.BeartypeDoorNonpepException` type, raised when the
    `beartype.door.TypeHint` constructor is passed an object that is
    *not* a PEP-compliant type hint currently supported by the DOOR API.
  Thanks so much to @tlambert03 for his phenomenal work here. He ran
  GitHub's PR gauntlet so that you did not have to. Praise be to him.
  Some people are the living embodiment of quality. @tlambert03 is one
  such people.
* **`beartype.peps`.** @beartype now publicizes runtime support for
  `typing`-centric Python Enhancement Proposals (PEPs) that currently
  lack official runtime support via a new public subpackage:
  `beartype.peps`. Notably, @beartype now provides:
  . Specifically, this commit:
  * A new public `beartype.peps.resolve_pep563()` function resolving
    [PEP 563][PEP 563]-postponed type hints on behalf of third-party
    Python packages. This function is intended to be "the final word" on
    runtime resolution of [PEP 563][PEP 563]. May no other third-party
    package suffer as we have suffered. This commit is for you,
    everyone. And "by everyone," we of course mostly mean @wesselb of
    [Plum](github.com/wesselb/plum) fame. See also beartype/plum#53.
* **`beartype.vale.Is*[...] {&,|}` short-circuiting.** `&`- and
  `|`-chained beartype validators now explicitly short-circuit when
  raising human-readable exceptions from type-checking violations
  against those validators, resolving issue #125 kindly submitted by
  typing brain-child @braniii.

## Features Optimized

* **`beartype.abby.is_bearable()` when returning `False`.** Previously,
  the public `beartype.abby.is_bearable()` runtime type-checker behaved
  reasonably optimally when the passed object satisfied the passed type
  hint but *extremely* suboptimally when that object violated that hint;
  this was due to our current naive implementation of that tester using
  the standard Easier to Ask for Permission than Forgiveness (EAFP)
  approach. This release fundamentally refactored
  `beartype.abby.is_bearable()` in terms of our new private
  `beartype._check.checkmake.make_func_tester()` type-checking tester
  function factory function. Ad-hoc profiling shows a speedup on
  the order of eight orders of magnitude – the single most intense
  optimization @beartype has ever brought to bear (*heh*). Our core code
  generation API now transparently generates both:
  * **Runtime type-checking testers** (i.e., functions merely returning
    ``False`` on type-checking violations).
  * **Runtime type-checking validators** (i.e., functions raising
    exceptions on type-checking violations).
* **[PEP 604][PEP 604]-compliant new unions** (e.g., `int | str |
  None`). Since these unions are **non-self-caching type hints** (i.e.,
  hints that do *not* implicitly cache themselves to reduce space and
  time consumption), @beartype now efficiently coerces these unions into
  singletons in the same manner as [PEP 585][PEP 585]-compliant type
  hints – which are similarly non-self-caching.

## Features Deprecated

* **`beartype.abby` → `beartype.door`.** This release officially
  deprecates the poorly named `beartype.abby` subpackage in favour of
  the sorta less poorly named `beartype.door` subpackage, whose name
  actually means something – even if that something is a punny acronym
  no one will ever find funny. Specifically:
  * `beartype.abby.die_if_unbearable()` has been moved to
    `beartype.door.die_if_unbearable()`.
  * `beartype.abby.is_bearable()` has been moved to
    `beartype.door.is_bearable()`.
  To preserve backward compatibility, the `beartype.abby` subpackage
  continues to dynamically exist (and thus be importable from) – albeit
  as a deprecated alias of the `beartype.door` subpackage.

## Deprecations Resolved

* **Setuptools licensing.** This release resolves a mostly negligible
  `setuptools` deprecation warning concerning the deprecated
  `license_file` setting in the top-level `setup.cfg` file. *Next!*

## Tests Improved

* **[PEP 544][PEP 544] compatibility.** All [PEP 544][PEP 544]-specific
  test type hints have been generalized to apply to both the non-caching
  `typing.Protocol` superclass *and* our caching
  `beartype.typing.Protocol` superclass.
* **[PEP 561][PEP 561] compatibility via pyright.** Our test suite now
  enforces static type-checking with `pyright`. Notably:
  * A new `test_pep561_pyright` functional test statically type-checks
    the @beartype codebase against the external `pyright` command in the
    current `${PATH}` (if available) specific to the version of the
    active Python interpreter currently being tested. For personal
    sanity, this test is currently ignored on remote continuous
    integration (CI) workflows. Let this shrieking demon finally die!
  * The private `beartype_test.util.cmd.pytcmdrun` submodule underlying
    our cross-platform portable forking of testing subprocesses now
    transparently supports vanilla Windows shells (e.g., `CMD.exe`,
    PowerShell).
* **Tarball compatibility.** `beartype` may now be fully tested from
  non-`git` repositories, including source tarballs containing the
  `beartype_test` package. Previously, three functional tests making
  inappropriate assumptions about the existence of a top-level `.git/`
  directory failed when exercised from a source tarball.
* **Sphinx documentation.** Our test suite now exercises that our
  documentation successfully builds with Sphinx via a new
  `test_sphinx_build()` functional test. This was surprisingly
  non-trivial – thanks to the `pytest`-specific `sphinx.testing`
  subpackage being mostly undocumented, behaving non-orthogonally, and
  suffering a host of unresolved issues that required we monkey-patch
  the core `pathlib.Path` class. Insanity, thy name is Sphinx.
* **GitHub Actions dependencies bumped.** This release bumps our GitHub
  Actions-based continuous integration (CI) workflows to both the
  recently released `checkout@v3` and `setup-python@v3` actions,
  inspired by a pair of sadly closed PRs by @RotekHandelsGmbH CTO
  @bitranox (Robert Nowotny). Thanks so much for the great idea,
  @bitranox!
* **`beartype.door` conformance.** A new smoke test guarantees
  conformance between our DOOR API and abstract base classes (ABCs)
  published by the standard `typing` module.
* **python/mypy#13627 circumvention.** This release pins our GitHub
  Actions-based CI workflow to Python 3.10.6 rather than 3.10.7,
  resolving a mypy-specific complaint inducing spurious test failures.

## Documentation Improved

* **[`beartype.abby`
  documented](https://github.com/beartype/beartype#beartype-at-any-time-api).**
  The new "Beartype At Any Time API" subsection of our front-facing
  `README.rst` file now documents our public `beartype.abby` API,
  resolving issue #139 kindly submitted by @gelatinouscube42 (i.e., the
  user whose username is the answer to the question: "What is the
  meaning of collagen sustainably harvested from animal body parts?").
* **[GitHub Sponsors activated](https://github.com/sponsors/leycec).**
  @beartype is now proudly financially supported by **GitHub Sponsors.**
  Specifically, this release:
  * Defines a new GitHub-specific funding configuration (i.e.,
    `.github/FUNDING.yml`).
  * Injects a hopefully non-intrusive advertising template
    <sup>*gulp*</sup> at the head of our `README.rst` documentation.
* **Sphinx configuration sanitized.** As the first tentative step
  towards chain refactoring our documentation from its current
  monolithic home in our top-level `README.rst` file to its eventual
  modular home at [ReadTheDocs (RTD)](https://beartype.readthedocs.io),
  en-route to resolving issue #8 (!) kindly submitted a literal lifetime
  ago by visionary computer vision export and long-standing phenomenal
  Finn @felix-hilden (Felix Hildén):
  * Our core Sphinx configuration has been resurrected from its early
    grave – which now actually builds nothing without raising errors. Is
    this an accomplishment? In 2022, mere survival is an accomplishment!
    So... *yes.* Significant improvements include:
    * Activation and configuration of the effectively mandatory
      `autosectionlabels` builtin Sphinx extension.
  * Our `doc/source/404.rst` file has been temporarily moved aside,
    resolving a non-fatal warning pertaining to that file. Look, we're
    not here to actually solve deep issues; we're here to just get
    documentation building, which it's not. Sphinx, you have much to
    answer for.
  * Our top-level `sphinx` entry point now:
    * Temporarily disables Sphinx's nit-picky mode (i.e., the `-n`
      option previously passed to `sphinx-build`) due to Sphinx's
      `autodoc` extension locally failing to generate working
      references.
    * Unconditionally disables Sphinx caching by forcing *all* target
      documentation files to be rebuilt regardless of whether their
      underlying source files have since been modified or not, obviating
      spurious build issues.

  [PEP 484]: https://www.python.org/dev/peps/pep-0484/
  [PEP 544]: https://www.python.org/dev/peps/pep-0544/
  [PEP 561]: https://www.python.org/dev/peps/pep-0561/
  [PEP 563]: https://www.python.org/dev/peps/pep-0563/
  [PEP 585]: https://www.python.org/dev/peps/pep-0585/
  [PEP 604]: https://www.python.org/dev/peps/pep-0604/
  [PEP 612]: https://www.python.org/dev/peps/pep-0612/
  [PEP 647]: https://www.python.org/dev/peps/pep-0647/

(*Impossible journey on an implacable placard-studded gurney!*)
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

Successfully merging a pull request may close this issue.

2 participants