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

Initial attempt a caching protocol implementation #86

Merged
merged 8 commits into from
Feb 14, 2022

Conversation

posita
Copy link
Collaborator

@posita posita commented Jan 22, 2022

Initial stab at porting numerary.types.CachingProtocolMeta to beartype. It only implements a naive cache that will grow indefinitely. (I.e., it does not include an LRU mechanism.) That can be addressed in a subsequent PR (before this one lands, if necessary). Further, this port does not include numerary's include/exclude mechanism, which should keep this implementation simpler.

Eventually, this should fix beartype/numerary#8.

Copy link
Collaborator Author

@posita posita left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Highlighted some questions/areas of concern. Nope. I committed a completely broken/incomplete thing. I fixed that and will highlight questions/areas of concern in the proper places. Apologies for the stealth-edit/commit rewrite.

For the future, please let me know if you prefer that I keep my commit(s) tidy (i.e., squash/rewrite as a go), or whether you prefer to preserve a longer commit chain here (complete with blunders and missteps) and tidy up at the end. Different maintainers have different preferences, so I want to make sure I don't mess up your flow.

UPDATE: I've been preserving commits in case I need to revert anything, but happy to tidy up later as desired.

Copy link
Collaborator Author

@posita posita left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Houston, we have a hiccup.¹ I forgot (or willed from my mind) some thorny issues around Protocols that may frustrate one of the goals of this PR, namely: Creating a drop-in replacement for typing.Protocol.

I'm not an expert, but as far as I can tell, typing.Protocol is a special animal. Really special. Like Chimera special.

For Mypy to work, I'm pretty sure it needs to think it's dealing with things directly derived from typing.Protocol and nothing else. Buuut for a world where others can gain access to caching behavior through mere subclassing, they need a parent with our metaclass implementation, which almost certainly isn't typing.Protocol.

I'm pretty sure the first import of typing.Protocol as Protocol "hides" our implementation from Mypy (hence the various # type: ignore [no-redef] comments), which tricks it into allowing static checking to proceed as normal. I think this is why we don't get static type-check errors for test_api_typing.py, even though it uses non-compliant "protocols" (i.e., those that derive from beartype.typing.Protocol without also deriving from typing.Protocol) all over the place.

That's a happy accident … for now. (I have no idea if that holds for other static type checkers.)

UPDATE: I believe I have settled on a solution (see my subsequent comment) with one minor compromise by putting implementations in their own module (_protocol.py for now) and conditionally importing them into typing.py.

The bigger issue is generic protocols. As far as I know, one can't have arbitrary generic placeholders. But typing.Protocol can. It behaves like typing.Generic in that regard. In other words, you can totally do this:

from abc import abstractmethod
from typing import Protocol, TypeVar, runtime_checkable
_AT_co = TypeVar("_AT_co", covariant=True)
_BT_co = TypeVar("_BT_co", covariant=True)
…
_NT_co = TypeVar("_NT_co", covariant=True)

@runtime_checkable
class SupportsOneTwo(Protocol[_AT_co, _BT_co, …, _NT_co]):  # <-- WTF?!
    @abstractmethod
    def a(self) -> _AT_co:
        pass
    @abstractmethod
    def b(self) -> _BT_co:
        pass@abstractmethod
    def n(self) -> _NT_co:
        pass

What I don't know how to do is to replicate typing.Protocol's ability to subscript arbitrary type parameters in our own class without requiring subclasses explicitly include Generic. So one could do this …

from typing import Generic
from beartype.typing import Protocol

class SupportsOneTwo(Protocol, Generic[_AT_co, _BT_co, …, _NT_co]):
    ...

… or this …

from typing import Protocol, runtime_checkable
from beartype.typing import _CachingProtocolMeta

@runtime_checkable
class SupportsOneTwo(Protocol[_AT_co, _BT_co, …, _NT_co], metaclass=_CachingProtocolMeta):
    ...

… or this …

import typing
from beartype.typing import Protocol

class SupportsOneTwo(Protocol, typing.Protocol[_AT_co, _BT_co, …, _NT_co]):
    ...

… but one cannot do this …

from beartype.typing import Protocol

class SupportsOneTwo(Protocol[_AT_co, _BT_co, …, _NT_co]):
    ...

You'll get a module loading error claiming that beartype.typing.Protocol is not generic. I'm stumped on how to get a syntax-compatible version. My hunch is that the best thing we can do in this case is tell customers to use Generic and not Protocol (i.e., the first alternative I presented above).


¹ Or "hiccough", if one prefers it. 🧐

beartype/typing.py Outdated Show resolved Hide resolved

# Define our own caching protocol
from typing import Protocol as _Protocol
# TODO: This is...a wart. Obviously, there is a fragility here. We should
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At worst, we could copy over the implementation from the standard library.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beartype/typing.py Outdated Show resolved Hide resolved
beartype/typing.py Outdated Show resolved Hide resolved
beartype/typing.py Outdated Show resolved Hide resolved
beartype/typing.py Outdated Show resolved Hide resolved
@posita
Copy link
Collaborator Author

posita commented Jan 23, 2022

Asking a related question here.

UPDATE: @ktbarrett directed my attention to this tiny, but important bit from PEP 544:

Protocol[T, S, ...] is allowed as a shorthand for Protocol, Generic[T, S, ...].

As he points out, there doesn't seem to be any other purpose for allowing Generic's magical notation to also apply to Protocol, which is unfortunate. But I think that works in favor of this compromise as being the recommended one, assuming we still want to go down this path:

from typing import Generic
from beartype.typing import Protocol

class SupportsOneTwo(Protocol, Generic[_AT_co, _BT_co, …, _NT_co]):
    ...

Regarding ensuring that Mypy thinks that beartype.typing.Protocol is actually typing.Protocol, we could field a .pyi file to that effect. Alternatively, we could do something like this:

obsolete example
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import Protocol
else:
    from typing import Protocol as _Protocol
    class Protocol(_Protocol, metaclass=_CachingProtocolMeta): …

UPDATE: As mentioned elsewhere in this PR, 6ad8629 pulls implementations into _protocols.py which are then conditionally imported into typing.py. I think this: a) makes the code cleaner; and b) explicitly addresses the issue of how Mypy "sees" beartype.typing.Protocol.

beartype/typing.py Outdated Show resolved Hide resolved
@posita
Copy link
Collaborator Author

posita commented Jan 24, 2022

6ad8629 pulls out the implementation into _protocol.py. I think this is cleaner. There are some redundant guarded imports, but that's part of the strategy. The idea is that _protocol.py conditionally creates the overrides, but in such a way that _protocol.py itself can be type-checked. typing.py handles the TYPE_CHECKING-guarded imports such that Mypy doesn't see beartype's overrides (for reasons alluded to in my prior comment). This is important to so that it treats beartype.typing.Protocol identically to Protocol. (The same goes for the various Supports protocols.)

The one wart here is that Mypy won't detect this problem:

>>> from beartype.typing import Protocol, TypeVar
>>> _T = TypeVar("_T")

>>> class MyProtocol(Protocol[_T]):  # <-- can't do this
>>>   pass

Users of beartype.typing.Protocol have to do this instead:

>>> from beartype.typing import Generic, Protocol, TypeVar
>>> _T = TypeVar("_T")

>>> class MyProtocol(Protocol, Generic[_T]):
>>>   pass

@leycec
Copy link
Member

leycec commented Jan 25, 2022

This right here straight-up Naruto run.

i advise against trying this

Yet again, @posita went above and beyond the bear call of duty. Aren't bears supposed to sleeping in a fitful hibernate slumber filled with berry bunches, bloodied muzzles, and swaggering female bear haunches this time of year anyway!?!?

Yet again, I am beyond the wall of sleep myself. This means I will remain verbose yet disconcertingly incoherent. Let's begin.

I know you have particular stylistic preferences for organization/naming/commenting.

...eye spasmodically twitches.

Blackest heresy! There is only one stylistic preference – and that is mine, I believe. All others are merely accidents their authors have yet to openly acknowledge. I'm kidding! Really. I'm not the unseemly jerk our neighbours continually claim me to be (however factual their video evidence may be).

Alternately, I'm obsessive-compulsive to the detriment of every codebase within keyboard reach of my pudgy fingers. I dispute neither claim.

If you're okay with it, I won't attempt to meet all those preferences here.

...very well. We reserve the Comfy Chair for another occasion then.

yes, oh yes

6ad8629 pulls out the implementation into _protocol.py.

You have done well, "young" protégé. I see your code is as big as mine.

Seriously, though. This rocks. For those who are about to code, I salute you and then genuflect until my back cracks in every single vertebrae simultaneously.

TYPE_CHECKING

Checkmate, mypy. Checkmate.

This implementation is ported straight across from numerary...

vigorous nod nod nod

...and does not limit entries.

...wut u say!?!?

I thought treating this as a functioning placeholder was a good place to start, though.

</ahem>

You are, of course, always correct.

vigorous nod nod nod

I kept the leading _ for now to not run afoul of the name checking in test_api_typing.py. I'm not quite sure I understand what's going on there or how to adapt to it, but we can discuss….

...heh. In hindsight, I probably should have documented that. With only minimal hand-waving, test_api_typing.py exists to exercise the constraint that there exists ...just get to it already a one-to-one-mapping between public exported attributes of beartype.typing and typing. If typing exports something that beartype.typing does not, test_api_typing.py raises a test failure.

I think. Maybe. Actually, let's not quote me on that.

You'll get a module loading error claiming that beartype.typing.Protocol is not generic.

Hindu elephant deity Ganesha preserve us.

That smells suspiciously like a poo-smelling deal-breaker. Let's see if we can't resolve this without any further reference to the Comfy Chair. Are you perhaps hitting this exception buried within the bowels of typing.py?

def _check_generic(cls, parameters, elen):
    """Check correct count for parameters of a generic cls (internal helper).
    This gives a nice error message in case of count mismatch.
    """
    if not elen:
        # v--- THIS BAD DUDE, RIGHT?
        raise TypeError(f"{cls} is not a generic class")
    alen = len(parameters)
    if alen != elen:
        raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments for {cls};"
                        f" actual {alen}, expected {elen}")

Since typing.Generic and typing.Protocol are pure-Python, we should (crossing pudgy fingers that kinda just rub against one another rather than crossing) be able to adroitly mimic the hideous incantation they're casting. Shut your eyes, Marion!

With the well-honed intuition of one who started violating privacy encapsulation when he was only 5, I intuit that the core issue is that ...just get to it already the Generic.__class_getitem__() dunder method not redefined by Protocol hardcodes itself and Protocol in an unsightly if conditional that makes that burst capillary in my eye throb with bloodshot viscosity even harder than it used to:

    @_tp_cache
    def __class_getitem__(cls, params):
        if not isinstance(params, tuple):
            params = (params,)
        if not params and cls is not Tuple:
            raise TypeError(
                f"Parameter list to {cls.__qualname__}[...] cannot be empty")
        params = tuple(_type_convert(p) for p in params)
        if cls in (Generic, Protocol):  # <-- THIS BAD DUDE RIGHT HERE
            # Generic and Protocol can only be subscripted with unique type variables.
            if not all(isinstance(p, (TypeVar, ParamSpec)) for p in params):
                raise TypeError(
                    f"Parameters to {cls.__name__}[...] must all be type variables "
                    f"or parameter specification variables.")
            if len(set(params)) != len(params):
                raise TypeError(
                    f"Parameters to {cls.__name__}[...] must all be unique")
        else:
            # Subscripting a regular Generic subclass.
            if any(isinstance(t, ParamSpec) for t in cls.__parameters__):
                params = _prepare_paramspec_params(cls, params)
            else:
                _check_generic(cls, params, len(cls.__parameters__))
        return _GenericAlias(cls, params,
                             _typevar_types=(TypeVar, ParamSpec),
                             _paramspec_tvars=True)

Let's circumvent that. Now hear me out here. Things gonna get ugly. Ever seen Reservoir Dogs? We're reproducing the ending right now.

class Protocol(...):
    ...

    def __class_getitem__(cls, params):
        super().__class_getitem__(_Protocol, params)  # <-- MAD CACKLING HERE

Pretty sure that'll work – but dead certain that'll fail. When it does, we consider options. Options that are labelled "DO NOT PUSH THIS SICKENINGLY RED BUTTON" include:

  • Redefining the entirety of Generic.__class_getitem__() as our own Protocol.__class_getitem__() that just stupidly replaces that stupid if cls in (Generic, Protocol): conditional with our homebrew if cls in (Generic, GodsHelpTypingAcceptOurSuperiorProtocolImplementationAlready):.
  • Temporarily monkey-patching typing.Protocol for the duration of that call. We are now literally reproducing Reservoir Dogs in Python by doing this, but we claim that typing leaves us no choice:
class Protocol(...):
    ...

    def __class_getitem__(cls, params):
        Prococol_old = typing.Protocol
        typing.Protocol = cls  # <-- MAD CACKLING HERE
        item = super().__class_getitem__(cls, params)  # <-- HERE AS WELL
        typing.Protocol = Protocol_old  # <-- CACKLING STILL AUDIBLE
        return item

this is so me

@posita
Copy link
Collaborator Author

posita commented Jan 26, 2022

Thank you for the education on object.__class_getitem__! Consider my mind sufficiently blown.

Well there's something you don't see every day.

I ain't too proud to beg [a system to do what I want by runtime patching¹]. At the risk of hitting the jolly, candy-like history eraser button, after looking at the implementation of Generic.__class_getitem__, I'm curious about the practical difference between something like this (adapted from above) …

class Protocol(...):
    ...

    def __class_getitem__(cls, params):
        Protocol_old = typing.Protocol
        try:
            typing.Protocol = cls
            return super().__class_getitem__(params)
        finally:
            typing.Protocol = Protocol_old

… and this (also adapted from above) …

class Protocol(...):
    ...

    def __class_getitem__(cls, params):
        _Protocol.__class_getitem__(params)

… ?²

One remote problem with the first approach might be a lack of thread safety. I assume that doesn't come up very often, but who knows? Something something … web frameworks … something something … runtime (re)loading of packages in separate threads … something something … dependency injection … [remainder deleted in the interest of time].

I think both encounter the LRU cache while masquerading as typing.Protocol. I don't know that I fully understand the implications. I'm also not confident I fully understand that implementation, but it seems like there aren't any side effects with cls, just what gets stuffed into the returned _GenericAlias (which will necessarily be typing.Protocol rather than beartype.typing.Protocol). I don't fully appreciate what that means either.³

UPDATE: I must have been tired. They're completely different. One invokes typing.Protocol.__class_getitem__, returns that value, and calls it a day. This doesn't work (as you suspected it wouldn't), but I think I accidentally salvaged the approach without fully understanding how it works. (See below.) The other invokes the same code path, but temporarily replaces typing.Protocol with ours during the execution, which (probably?) results in a (more) correct(ish) _GenericAlias being returned (maybe).

That being said, I see your Reservoir Dogs ending and raise you a Taxi Driver with:

class Protocol(_Protocol, …):
    # …
    def __class_getitem__(cls, params):
        gen_alias = _Protocol.__class_getitem__(params)
        return type(gen_alias)(cls, *gen_alias.__args__)

UPDATE: I am also spent (and I haven't even gotten to "Beyond the Wall of Sleep" yet), but I did throw up gently place in view a version of the above for critique.


¹ I concede that starts to sound less like begging and more like coercing, but let's leave that be for the time being.

² Mad cackling omitted for brevity, but assume it persists throughout the quoted material.

³ I have a lot of skepticism around my own knowledge in this area, if you didn't catch that theme.

beartype/_protocol.py Outdated Show resolved Hide resolved
@posita posita force-pushed the posita/0/caching-protocol branch from 8094469 to fdd4a82 Compare January 27, 2022 03:56
@codecov
Copy link

codecov bot commented Jan 27, 2022

Codecov Report

Merging #86 (0eace34) into main (b3d9af9) will decrease coverage by 0.09%.
The diff coverage is 89.61%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #86      +/-   ##
==========================================
- Coverage   96.69%   96.59%   -0.10%     
==========================================
  Files         118      119       +1     
  Lines        4387     4464      +77     
==========================================
+ Hits         4242     4312      +70     
- Misses        145      152       +7     
Impacted Files Coverage Δ
beartype/typing.py 90.00% <57.14%> (-10.00%) ⬇️
beartype/_protocol.py 92.85% <92.85%> (ø)
...ype/_decor/_error/_pep/_pep484585/_errorgeneric.py 90.00% <0.00%> (+3.33%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b3d9af9...0eace34. Read the comment docs.

@posita posita force-pushed the posita/0/caching-protocol branch 5 times, most recently from 097c39b to 9a9b4a8 Compare January 27, 2022 04:21
beartype/_protocol.py Show resolved Hide resolved
beartype/typing.py Show resolved Hide resolved
@leycec
Copy link
Member

leycec commented Jan 28, 2022

This is amazing fiery balls, if you hadn't already noticed. 🌞

But first!

I'm not sure why Mypy chokes on this in CI for Python 3.7.

Because...

this is mypy

Because mypy is just a pseudonym for He Who Walks Behind the Rows, let's pretend mypy isn't watching like a voyeur in the corn fields for now. As time permits and the cats finally stop howling for food, we can uppercut mypy's glass jaw with a spray of # type: ignore[EVERYTHING. IGNORE EVERYTHING, MYPY.] comments in beartype.typing.

For now, we glare sternly in mypy's general direction.

But next!

       def __class_getitem__(cls, params):
            gen_alias = _Protocol.__class_getitem__(params)

            # I'm pretty sure we need this nudge. Otherwise our inheritors end
            # up with the wrong metaclass (i.e., type(typing.Protocol) instead
            # of the desired _CachingProtocolMeta). Luddite alert: I don't
            # fully understand the mechanics here.
            return type(gen_alias)(cls, *gen_alias.__args__)

You have now gone to a place where none but eagles dare.

You almost got there without burning your microlight wings made of greasy (yet patented) paraffin wax in the positronic radiation field emitted by the standard typing module – sorta like how I almost made my wife proud by cross-country skiing only five times as slow as her today. It's a special milestone for me.

The burning concern here is the instantiation of _GenericAlias. We're dropping a few parameters we really shouldn't be. Now the shambling horde of unkillable zombies whose only weakness is a blowtorch are gathering about the absolutely safe first-floor mall food court we're desperately pilfering for penicillin-encrusted snow cones and last month's sushi.

Under Python 3.10, here's how Protocol crazily instantiates _GenericAlias:

    @_tp_cache
    def __class_getitem__(cls, params):
        ...
        return _GenericAlias(cls, params,
                             _typevar_types=(TypeVar, ParamSpec),
                             _paramspec_tvars=True)

So Protocol is instantiating _GenericAlias with non-default values for both _typevar_types and _paramspec_tvars. I definitely know exactly what those parameters signify but have omitted an extensive explanation due to... space constraints. Yes. Space constraints, I'm afraid. nervous hand wringing

I'm now squinting suspiciously at *gen_alias.__args__, too. If _GenericAlias.__init__() can be trusted, nervous eye twitching *gen_alias.__args__ should basically be equivalent to *params – which definitely doesn't seem right, because:

  • params should be passed non-variadically. I think. Maybe. Actually, I have no idea what's happening here anymore. When nothing has meaning, everything has meaning.
  • We're not passing the right non-default values for either _typevar_types or _paramspec_tvars. Caveat emptor: I can barely remember my own name at this point. I'm pretty sure it's Yojimbo Baggins... but not certain.

Because I'm now squinting suspiciously at everything, let's slowly backtrack to the last point in the autumn woods not inhabited by unsettling effigies, cawing crows, and a furtive shadow at the corner of your eye that keeps shifting even as you uneasily pretend "...it's really not there, it's not really there."

This is that point:

class Protocol(...):
    ...

    def __class_getitem__(cls, params):
        Protocol_old = typing.Protocol
        try:
            typing.Protocol = cls
            return super().__class_getitem__(params)
        finally:
            typing.Protocol = Protocol_old

this is galaxy brain

This is galaxy brain. Unlike my plebeian proposal, your upper-class approach cleverly accounts for unexpected exceptions.

Let's rock this like Rosemary's baby in the cradle.

@posita posita force-pushed the posita/0/caching-protocol branch from 9a9b4a8 to ea7b861 Compare January 29, 2022 10:04
@posita
Copy link
Collaborator Author

posita commented Jan 29, 2022

This is that point: … Let's rock this like Rosemary's baby in the cradle.

Your wish is my command…eventually. Allow me first to draw attention to one (perceived) flaw of Rosemary's Galaxy Baby, which may amount to a nothingburger, as well as one last delve into my misguided hijack-the-generic-alias approach.

Theoretical race condition present in the we're-going-to-replace-that-faberge-egg-with-a-fake-but-just-for-a-minute-no-one-will-notice-we-promise approach

Here is the race I am worried about with the above (which I'll rewrite slightly).

class Protocol(...):
  ...
  def __class_getitem__(cls, params):

    # Thread 1
    Protocol_old = typing.Protocol
    typing.Protocol = cls
                                                    # Thread 2
                                                    Protocol_old = typing.Protocol
                                                    typing.Protocol = cls
    try:
      return super().__class_getitem__(params)
    finally:
      typing.Protocol = Protocol_old
                                                    try:
                                                      return super().__class_getitem__(params)
                                                    finally:
                                                      typing.Protocol = Protocol_old

Consider:

import typing
from abc import abstractmethod
from functools import partial
from threading import Thread
from typing import TypeVar
from beartype.typing import Protocol

T = TypeVar("T")

contrived_example = """
class ThisIsNutsButMayBePossible{}(Protocol[T]):
  @abstractmethod
  def foo(self) -> None:
    pass
"""

def compileit(i: int):
    exec(contrived_example.format(i))

threads = [Thread(target=partial(compileit, i)) for i in range(100)]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print(typing.Protocol)  # <-- what result?

If I run that, I get the output <class 'typing.Protocol'> pretty consistently until I add a print() after the typing.Protocol = cls line in our __class_getitem__ implementation. Then I get <class 'beartype._protocol.Protocol'>, also pretty consistently. Now I concede that this is contrived (yes, I know I'm deliberately triggering an interrupt with the artificial print() in the middle), and that this may be a non-existent risk in the real world.

Yojimbo Baggins was right - revisiting creating and gutting the _GenericAlias

You are, as usual, completely justified in your suspicions. My use of *gen_alias.__args__ was just flat out wrong. Further, as you point out, _typevar_types and _paramspec_tvars (introduced in Python 3.10) are MIA.

I thought of doing something like this:

        def __class_getitem__(cls, params):
            gen_alias = _Protocol.__class_getitem__(params).
            gen_alias.__origin__ = cls
            return gen_alias

But that is…problematic. From Protocol.__class_getitem:

    @_tp_cache  # <-- uh-oh
    def __class_getitem__(cls, params):
        …
        return _GenericAlias(cls, params,
                             _typevar_types=(TypeVar, ParamSpec),
                             _paramspec_tvars=True)

The value returned from typing.Protocol.__class_getitem__(params) is cached. If we change that value, later calls to typing.Protocol.__class_getitem__ with the same params will get our hacked up version, even if they are not from our our stand-in. 🤦

This - nope. This is the best alternative to swapping out typing.Protocol @TeamSpen210 and I can come up with.

If that meets not with your approval and we should instead proceed with Operation Swap Galaxies but Only for the Tiniest of Whiles, just say the word, and it shall be done. I live but to serve. 🙇

Galaxy Bear
Galaxy Bear knows all, sees all, and yet is still somehow fascinated by diminutive ponies.

@posita posita force-pushed the posita/0/caching-protocol branch from ea7b861 to fbb2cc6 Compare January 29, 2022 10:44
@leycec
Copy link
Member

leycec commented Feb 5, 2022

The review to end all reviews: it's beginning.

popcorn please

@leycec
Copy link
Member

leycec commented Feb 7, 2022

Ohnoes. All pending work for beartype 0.10.0 has been shipped to the cleaners and is now waiting for me to just tag the most recent commit and falsify up accurately describe the changelog – which is huge, by the way. What a cray-cray release cycle that was, eh? Eh? ← so much 🇨🇦 right there

Tragically, I took so long to review this that this probably won't land in beartype 0.10.0. I deeply and regretfully apologize to @posita and @TeamSpen210 with mournful tears streaming into my dinner, which now tastes disgustingly salty.

Thankfully, this is my top priority for the inevitable beartype 0.10.1 point release. I will make this happen... or my name isn't Piston Sam! 🚂 🚂 🚂

@posita
Copy link
Collaborator Author

posita commented Feb 7, 2022

Tragically, I took so long to review this that this probably won't land in beartype 0.10.0. I deeply and regretfully apologize to @posita and @TeamSpen210 with mournful tears streaming into my dinner, which now tastes disgustingly salty.

Thankfully, this is my top priority for the inevitable beartype 0.10.1 point release. I will make this happen... or my name isn't Piston Sam! 🚂 🚂 🚂

I think the delay is good, and I wouldn't kill myself to squeeze this into 0.10.1. First, we might want to consider waiting until CPython gets is copy_with fix. (Of course, we might be waiting for awhile, so maybe that's a bad strategy.) Second, I think I'd feel comfortable with some time/space to test this out in some real world applications (which I haven't yet done to the extent I would like). Should I put this back in "draft" mode?

@leycec
Copy link
Member

leycec commented Feb 8, 2022

First, we might want to consider waiting until CPython gets its copy_with fix.

This is my preferred life strategy. Begging my betters to do things for me has yet to fail. B-b-ut...

(Of course, we might be waiting for awhile, so maybe that's a bad strategy.)

...this. The CPython world moves like glacial meltwater off the Canadian permafrost infested with ancient virii first envisioned by John Carpenter.

if you burn it, it can die
Python 3.10 release cycle dev struggle is real

We'll be lucky to ram this thing through Python 3.11. Meanwhile, Python 3.7 through 3.10 still exist. (Python 3.6: you are dead to me.)

Thanks again for the tremendous work, everyone. I'm deathly afraid of breaking this PR and thus repressing my earnest desire to fundamentally refactor beartype.typing into something horrifying.

I... I don't know how long I can hold on, guys. 😓 😰 🥵

@leycec
Copy link
Member

leycec commented Feb 14, 2022

@posita: On second thought, this is good to go. Don't sweat the overly fine minutiae and my banal code review. I'm all for banal, let's be clear – but you've already been around the bend (and back again) with @TeamSpen210.

I'll add a few internal FIXME: comments for one or both of us to eventually resolve these minor pending concerns, which can absolutely wait until then:

  • The if base is cls or base.__name__ in (...): branch probably erroneously matches unrelated user-defined types whose names just happen to be Generic or Protocol. Ideally, we should tighten that up to only match the actual {beartype,}.typing.{Generic,Protocol} types.
  • The call to typing._get_protocol_attrs() violates privacy encapsulation, which has me cueing up Megadeth's Sweating Bullets on the junkyard vinyl playlist. Ideally, we should copy-and-paste that function into this private submodule instead.

If heaven can wait, ...Alexa says it can so can this.

Thunderous applause for this third out of three tremendous contributions, @posita. Likewise, thanks so much for the grand critique that helped fuel this along in my awkward and questionable absence, @TeamSpen210.

You both are the stuff open-source dreams are made of. 🕺 💃

@leycec leycec merged commit 2230c68 into beartype:main Feb 14, 2022
@posita
Copy link
Collaborator Author

posita commented Feb 15, 2022

  • The if base is cls or base.__name__ in (...): branch probably erroneously matches unrelated user-defined types whose names just happen to be Generic or Protocol. Ideally, we should tighten that up to only match the actual {beartype,}.typing.{Generic,Protocol} types.
  • The call to typing._get_protocol_attrs() violates privacy encapsulation, which has me cueing up Megadeth's Sweating Bullets on the junkyard vinyl playlist. Ideally, we should copy-and-paste that function into this private submodule instead.

☝️ This.

Your points are well taken. If I get some time to try to nudge things into a better place, I will (although _get_protocol_attrs is going to give me heartburn either way). Alternatively, if a beartype customer has problems with these, I will come to provide aid as soon as I can. I should be able to tackle the base.__name__ issue shortly. The standard library makes a similar blunder in…wait for it_get_protocol_attrs, so maybe I can tackle both problems at once….

@leycec
Copy link
Member

leycec commented Feb 15, 2022

...although _get_protocol_attrs() is going to give me heartburn either way...

Just got there fifteen minutes ago. Thanks to typing, I burn with the incandescent fury of a thousand suns imploding into the gaping maw of the Andromeda Galaxy.

True worst-case end times story: when our galaxy collides with Andromeda in 5 billion years, armchair Wikipedia astrologists believe that "...the Sun might be brought near the centre of the combined galaxy, potentially coming near one of the black holes before being ejected entirely out of the galaxy.[13] Alternatively, the Sun might approach one of the black holes a bit closer and be torn apart by its gravity. Parts of the former Sun would be pulled into the black hole."

Is this supposed to reassure us, Wikipedia?

Are we just ping-pong balls to you, Wikipedia?

Back to the issue front. I realize I no longer understand the _check_only_my_attrs() helper. If we combine its powers with _get_protocol_attrs(), a terrifying new kaiju emerges from the radioactive dust plume:

    def _check_only_my_attrs(cls, inst: Any) -> bool:
        # So, "dict" actually supports set operators under
        # Python >= 3.9. Ignoring Python < 3.9 just cause,
        # this *probably* reduces to this one-liner:
        #     attrs = set(cls.__dict__ | cls.__dict__.get("__annotations__", {}))
        # We may not even need to coerce "attrs" to a set.
        # Dictionaries are now pretty much sets, but hotter.
        # *ANYWAY.* This makes sense; we're getting a
        # set of all unique attribute names. Good!
        attrs = set(cls.__dict__)
        attrs.update(cls.__dict__.get("__annotations__", {}))

        #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        # Inlining _get_protocol_attrs(): it begins.
        #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

        # Uh... This is why we should inline. Let's pretend
        # this redefinition never happens. Eye is twitching.
        base_attrs = set()

        # *UH OH.* For all classes, the following tautology
        # holds:
        #     >>> cls.__mro__[0] is cls
        #     True
        # This means that the first "base" visited by this
        # iteration is just the passed "cls". But, wait. We
        # just introspected the attributes of "cls" above!
        # Clearly, we don't need to do that above. Let's
        # centralize all of the attribute inspection here.
        #
        # Very well. We progress deeper into the dimly
        # lit cavern from which grunting can be heard.
        for base in cls.__mro__[:-1]:  # without object
            # Guido, you cause me pain.
            if base.__name__ in ('Protocol', 'Generic'):
                continue

            # *WTFBRO.* What is this madness? Coercing
            # "KeyView" containers into lists and then
            # appending them into an even larger list?
            # This is a facepalm. This is why we inline.
            # Lastly, "{}" is not a singleton –
            # unlike "()", which is. Don't ask. So, we
            # declare a stupid empty global dictionary
            # singleton as a stupid hidden parameter: e.g.,
            #     def _check_only_my_attrs(
            #         cls, inst: Any, _empty_dict = {}) -> bool:
            #
            # This should then suffice with speed and glamour:
            #     base_dict = base.__dict__
            #     cls_attrs = base_dict | base_dict.get('__annotations__', _empty_dict)
            #     for attr in cls_attrs: ...
            #
            # Grunting sounds segue into mewling, hissing,
            # and a blood-curdling falsetto abruptly cut short.
            annotations = getattr(base, '__annotations__', {})
            for attr in list(base.__dict__.keys()) + list(annotations.keys()):
                # This is fine. It is the only thing that is.
                if not attr.startswith('_abc_') and attr not in EXCLUDED_ATTRIBUTES:
                    base_attrs.add(attr)

        #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        # Inlining _get_protocol_attrs(): it ends.
        #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

        # I begin to question basic assumptions like
        # whether my 30 year-old username being a
        # portmanteau of leyline and Cecil is really
        # such a good idea after all.
        #
        # I've now officially lost the entire plot.
        # Doesn't this statement just reduce to:
        #     attrs = attrs
        # That is, doesn't intersection_update()
        # just remove all attribute names that are
        # *NOT* in both sets? But "base_attrs" is
        # the proper superset of "attrs". So, this
        # just removes all attribute names except
        # those directly defined by the passed "cls".
        # In other words, the entire doubly-nested
        # "for" loop we performed above was a noop.
        #
        # The sound of wet bloody meat being forcibly
        # dragged across a rough surface of stalagmites
        # percolates through the fetid crypt-like air.
        attrs.intersection_update(base_attrs)  # type: ignore [attr-defined]

        # And... we're iterating above. Pretty sure we can
        # just perform this test in the iteration above. That
        # is, I *would* be pretty sure if I was pretty sure
        # about anything here. But I'm not.
        #
        # Oppressive grunting sounds resume.
        for attr in attrs:
            if (
                not hasattr(inst, attr) or
                (
                    callable(getattr(cls, attr, None)) and
                    getattr(inst, attr) is None
                )
            ):
                return False

        return True

Kinda uncertain about core scruples and basic worldview anymore, but suspect this alternative trash compactor might get us past the abyss:

    from typing import Generic
    _BASES_IGNORABLE = frozenset((
        Generic, Protocol, _Protocol))

    def _check_only_my_attrs(
        cls,
        inst: Any,
        _empty_dict = {},
    ) -> bool:

        for base in cls.__mro__[:-1]:  # without object
            # Guido, you cause me pain.
            if base in _BASES_IGNORABLE:
                continue

            base_dict = base.__dict__
            base_annotations = base_dict.get('__annotations__', _empty_dict)
            base_attrs = (
                base_dict | base_annotations
                if IS_PYTHON_AT_LEAST_3_9 else
                dict(base_dict, **base_annotations)
            )

            for attr in base_attrs:
                if (
                    # If this attribute name is unignorable *AND*...
                    (
                        not attr.startswith('_abc_') and
                        attr not in EXCLUDED_ATTRIBUTES
                    ) and
                    # Either...
                    (
                        # This instance does not define this attribute *OR*
                        not hasattr(inst, attr) or (
                            # This attribute is a callable method defined on
                            # this class instead *AND*...
                            callable(getattr(cls, attr, None)) and
                            #FIXME: Must confess, I still have no idea what
                            #this specific test is on about. Back to the abyss!
                            # This attribute is... none? I don't. I just don't.
                            getattr(inst, attr) is None
                        )
                    )
                ):
                    # This instance fails to define this attribute and thus
                    # fails to satisfy this protocol. In this case, we fail.
                    return False

        # Else, this instance defines all attributes and thus satisfies this
        # protocol. In this case, we succeed.
        return True

In theory, being optimistic here fellahs we don't even need to keep a running attrs set. We just validate protocolhood in-place while clutching an empty vodka bottle and staring off blankly at the burned-out shell of a CRT monitor.

The standard library makes a similar blunder in…wait for it…_get_protocol_attrs.

Rictus grimace: triggered. 😬

Since you understandably find no deeper fulfilment in life than reporting CPython issues, i am this way too would you mind taking one for all humanity by throwing yourself onto the sword that is the public CPython bug tracker? @TeamSpen210 and I loudly cheer from the sidelines at a distance.

It's a disturbing shocker that this is still in CPython 3.10. The tenuous compassion for all mankind I normally, of course, feel has slipped yet another femptometer closer to the abyssal cliff of Nietzschean despair.

abyss is cat

@leycec
Copy link
Member

leycec commented Feb 16, 2022

ohisortagetitnowbutnotreally

You... you are bloody ingenious, @posita. We already knew that but I had to personally confirm that. Specifically, I had to judiciously pollute your _check_only_my_attrs() function with untold print() statements to even begin to comprehend the dark mysteries unfolding before me as I slumped over the keyboard.

The ominous key appears to be that CPython implicitly calls the metaclass __instancecheck__() method once for each subclass of a beartype.typing.Protocol hierarchy – each of which necessarily shares your same caching metaclass and thus that method. I think? If I'm reading these print() statements correctly? I'm probably not.

That's fascinating if true. I'd assumed CPython would only call __instancecheck__() once for the entire hierarchy, which only goes to show that I resemble a donkey. My wife agrees.

EDIT: ZOMG. Of course, my donkey-like assumption wasn't without merit. CPython does only call __instancecheck__() once for the entire hierarchy by default – which is why you iterate over the superclasses of the passed protocol and then forcefully isinstance() each of them, which then induces recursion by indirectly invoking the same __instancecheck__() method. I am Jack's Valentine's Day exploding head. 😑 🤯

That... that's insane but so clever my brains are leaking out my itchy scalp, bro. I'd better document all of this immediately before I lose my feeble grip and succumb to this shadow madness.

Please Send Help for I Am Tired and It's Cold

I still have no clear idea what exactly this does, though:

        attrs.intersection_update(_get_protocol_attrs(cls))  # type: ignore [attr-defined]

Clearly, that's essential. I know this because the Abyss opens up when I comment that out.

Clearly, I'm also incapable of fully comprehending the eternal verities that you have authored here. Please enlighten, Beneficent Protocol Guru, that I may comment this and then inline _get_protocol_attrs() here forthwith.

@posita posita deleted the posita/0/caching-protocol branch February 16, 2022 13:59
@leycec
Copy link
Member

leycec commented Feb 17, 2022

Hah! We see you deleting branches over there. (≖_≖ )

@leycec
Copy link
Member

leycec commented Feb 17, 2022

Buh-yah. Finally untangled the rat's nest. Behold a type-checking subroutine for caching protocols respectful of privacy (and faster on that first uncached hit, too):

    def _check_only_my_attrs(cls, inst: Any, _EMPTY_DICT = {}) -> bool:

        cls_attr_name_to_value = cls.__dict__
        cls_attr_name_to_hint = cls_attr_name_to_value.get(
            '__annotations__', _EMPTY_DICT)
        cls_attr_names = (
            cls_attr_name_to_value | cls_attr_name_to_hint
            if IS_PYTHON_AT_LEAST_3_9 else
            dict(cls_attr_name_to_value, **cls_attr_name_to_hint)
        )

        # For the name of each attribute declared by this protocol class...
        for cls_attr_name in cls_attr_names:
            # If...
            if (
                # This name implies this attribute to be unignorable *AND*...
                #
                # Specifically, if this name is neither...
                not (
                    # A private attribute specific to dark machinery defined by
                    # the "ABCMeta" metaclass for abstract base classes *OR*...
                    cls_attr_name.startswith('_abc_') or
                    # That of an ignorable non-protocol attribute...
                    cls_attr_name in _PROTOCOL_ATTR_NAMES_IGNORABLE
                # This attribute is either...
                ) and (
                    # Undefined by the passed object *OR*...
                    #
                    # This method has been specifically "blocked" (i.e.,
                    # ignored) by the passed object from being type-checked as
                    # part of this protocol. For unknown and presumably
                    # indefensible reasons, PEP 544 explicitly supports a
                    # fragile, unreadable, and error-prone idiom enabling
                    # objects to leave methods "undefined." What this madness!
                    not hasattr(inst, cls_attr_name) or
                    (
                        # A callable *AND*...
                        callable(getattr(cls, cls_attr_name, None)) and
                        # The passed object nullified this method. *facepalm*
                        getattr(inst, cls_attr_name) is None
                    )
                )
            ):
                # Then the passed object violates this protocol. In this case,
                # return false.
                return False

        # Else, the passed object satisfies this protocol. In this case, return
        # true.
        return True

@posita: Uhohs. Turns out caching protocols fail to support a standard use case of subscription by non-type variables satisfying a type variable: e.g.,

# This is fine.
>>> from typing import AnyStr, Protocol
>>> class GoodProtocol(Protocol[AnyStr]): pass
>>> class BadProtocol(GoodProtocol[str]): pass

# This blows fine chunks across my keyboard.
>>> from beartype.typing import AnyStr, Protocol
>>> class GoodProtocol(Protocol[AnyStr]): pass
>>> class BadProtocol(GoodProtocol[str]): pass
Traceback (most recent call last):
  File "mopy.py", line 7, in <module>
    class BadProtocol(GoodProtocol[str]): pass
  File "/home/leycec/py/beartype/beartype/typing/_typingpep544.py", line 403, in __class_getitem__
    gen_alias = _Protocol.__class_getitem__.__wrapped__(
  File "/usr/lib/python3.8/typing.py", line 890, in __class_getitem__
    raise TypeError(
TypeError: Parameters to Protocol[...] must all be type variables

Do numerary protocols suffer a similar issue? You know it's Wednesday when you feel like this. 😮‍💨

@posita
Copy link
Collaborator Author

posita commented Feb 17, 2022

Wow…that is some heavy lifting! I hope you did not suffer permanent injury.

My apologies for my lack of response on this thread. (Pesky day job has me runnin' wild.) I promise to circle back as soon as I get a minute! 😅

P.S. Don't worry about branches. New ones are easy to make. 😉 Oh wait. I hadn't considered that you might be working with it as well. My bad if you were! I made the assumption you were tinkering elsewhere. I restored it just in case.

@posita posita restored the posita/0/caching-protocol branch February 17, 2022 18:56
@leycec
Copy link
Member

leycec commented Feb 17, 2022

Oh, no worries whatsoever! Work-life balance is no balance when you're teetering on the edge. I just accidentally resolved the above issue, too. I don't entirely understand the solution, which is par for the course around here:

        # Newer, better, faster, harder __class_getitem__()
        # builds a brighter world despite watering eyes and
        # increased sanity loss.
        def __class_getitem__(cls, item):

            # We have to redefine this method because typing.Protocol's version
            # is very persnickety about only working for typing.Generic and
            # typing.Protocol. That's an exclusive club, and we ain't in it.
            # (RIP, GC.) Let's see if we can sneak in, shall we?

            # FIXME: Once <https://bugs.python.org/issue46581> is addressed,
            # consider replacing the madness below with something like:
            #   cached_gen_alias = _Protocol.__class_getitem__(_Protocol, params)
            #   our_gen_alias = cached_gen_alias.copy_with(params)
            #   our_gen_alias.__origin__ = cls
            #   return our_gen_alias

            # We can call typing.Protocol's implementation directly to get the
            # resulting generic alias. We need to bypass any memoization cache
            # to ensure the object on which we're about to perform surgery
            # isn't visible to anyone but us.
            if hasattr(_Protocol.__class_getitem__, '__wrapped__'):
                # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                # @posita: New magic happens here.
                # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                base_cls = (_Protocol if _Protocol in cls.__bases__ else cls)
                gen_alias = super().__class_getitem__.__wrapped__(
                    base_cls, item)
            else:
                # We shouldn't ever be here, but if we are, we're making the
                # assumption that typing.Protocol.__class_getitem__ no longer
                # caches. Heaven help us if it ever uses some proprietary
                # memoization implementation we can't see anymore because it's
                # not based on functools.wraps.
                gen_alias = _Protocol.__class_getitem__(item)

            # Now perform origin-replacement surgery. As-created,
            # gen_alias.__origin__ is (unsurprisingly) typing.Protocol, but we
            # need it to be our class. Otherwise our inheritors end up with
            # the wrong metaclass for some reason (i.e., type(typing.Protocol)
            # instead of the desired _CachingProtocolMeta). Luddite alert: I
            # don't fully understand the mechanics here. I suspect no one does.
            gen_alias.__origin__ = cls

            # We're done! Time for a honey brewskie break. We earned it.
            return gen_alias

Because this makes my eyes water, this might make your eyes water, too. I recant everything I said about @beartype and numerary "going their own ways" on caching protocol implementations.

Would you like to unify our two approaches? As I dimly recall, our current caching protocol API almost satisfied your use cases – except for include() and exclude() methods, I think? If so, perhaps we might either add those methods to our API or enable numerary to safely subclass, monkey-patch, or otherwise add those methods to our API.

Because you're Bear Clan, you have full carte blanche to:

  • Add anything you like to beartype.typing.Protocol, even if numerary is the only thing that needs those things. Things: lotsa things.
  • Publicize any currently private attributes of our caching protocol API, especially _CachingProtocolMeta (if you need it, of course).

It's a full moon out there and we're howling at it. 🌝

@leycec
Copy link
Member

leycec commented Feb 19, 2022

For better or worse, this just got pushed out the door when no one was looking with beartype 0.10.1 thanks to a critical defect with respect to positional-only parameters. Will beartype.typing.Protocol actually behave itself or am I sitting all alone on another miswired timebomb? 💣 💥

You decide:

$ pip install --upgrade beartype

@posita
Copy link
Collaborator Author

posita commented Feb 21, 2022

Playing catch-up…. Do I understand from beartype/typing/_typingpep544.py that the approach you outlined in #86 (comment) has made it into main (and also 0.10.1)? I think that's the case, but I want to be sure I understand the context.

In other words, is your question: "Did I just trigger the doomsday machine by releasing this into the world, or are we still cool and I should learn to stop worrying and love the bomb?" I want to make sure I'm responding to the right thing….

UPDATE: I now have a branch that strips out the base implementation of the caching protocol from numerary and instead completely replaces it with yours. I'm not sure I have a better way to gauge confidence in your modifications other than numerary's tests all still pass. 🎉

🙌
🥳

numerary loses 3.7 support in the process, but I think that's worth it.

posita added a commit to beartype/numerary that referenced this pull request Feb 21, 2022
... and separate override functionality from caching functionality. This will be useful
if beartype/beartype#86 lands and we decide to use that version (somehow) instead of
replicate our own.
posita added a commit to beartype/numerary that referenced this pull request Feb 21, 2022
Now that beartype/beartype#86 has landed (and made it into a release), we can remove our own base implementation and use that one instead. We still provide our own implementation that allows overriding runtime checking, but it neatly derives from ``beartype``'s.
posita added a commit to beartype/numerary that referenced this pull request Feb 22, 2022
Now that beartype/beartype#86 has landed (and made it into a release), we can remove our own base implementation and use that one instead. We still provide our own implementation that allows overriding runtime checking, but it neatly derives from ``beartype``'s.
posita added a commit to beartype/numerary that referenced this pull request Feb 22, 2022
Now that beartype/beartype#86 has landed (and made it into a release), we can remove our own base implementation and use that one instead. We still provide our own implementation that allows overriding runtime checking, but it neatly derives from ``beartype``'s.
posita added a commit to beartype/numerary that referenced this pull request Feb 22, 2022
Now that beartype/beartype#86 has landed (and made it into a release), we can remove our own base implementation and use that one instead. We still provide our own implementation that allows overriding runtime checking, but it neatly derives from ``beartype``'s.
posita added a commit to beartype/numerary that referenced this pull request Feb 22, 2022
Now that beartype/beartype#86 has landed (and made it into a release), we can remove our own base implementation and use that one instead. We still provide our own implementation that allows overriding runtime checking, but it neatly derives from ``beartype``'s.
posita added a commit to beartype/numerary that referenced this pull request Feb 22, 2022
Now that beartype/beartype#86 has landed (and made it into a release), we can remove our own base implementation and use that one instead. We still provide our own implementation that allows overriding runtime checking, but it neatly derives from ``beartype``'s.
@leycec
Copy link
Member

leycec commented Feb 22, 2022

Do I understand from [BIGLY URL] that the approach you outlined in [OTHER BIGLY URL] has made it into main (and also 0.10.1)?

You understand everything. Thus, you understand this.

Actually, I'm jelly at how fast you grok code even in the midst of a feverish working pitch, full-throttle family fun, and open-source commitments beyond even my most wide-eyed idealism.

You dah bro. That's what I'm saying here.

numerary loses 3.7 support in the process, but I think that's worth it.

🤕

On the bright side, Python 3.7 hits EOL in a year-and-a-half. On the dark side, something worked and now it doesn't.

I'm actually curious how you achieved that when... oh. That is right. I recall it like it was yesterday's fetid goat cheese. The private typing._Protocol class in Python 3.7, right? Is that class actually a perfectly working drop-in replacement for typing.Protocol in Python ≥ 3.8? If so:

  • That's crazy. Why hide beautiful things, Guido?
  • Let's use that. Specifically, let's just substitute typing._Protocol for typing.Protocol like you probably currently do in numerary. Would that suffice?

It goes without saying, but I'll say it: "Please feel free to have a go at everything in the beartype.typing subpackage. Mi casa es su casa, because we built this house together. Okay, it was mostly you."

Relatedly:

This decision was not made lightly. numerary is intended as a temporary work-around. It’s obsolescence will be something to celebrate.

🥳

Wait just a hot minute! That's not something to celebrate. numerary rocks. Since numerary rocks and you intend to quietly axe it out back, would you prefer to just shift its intestines into an appropriate @beartype subpackage? For example:

  • beartype.numbers. Right? Right? Hear me out here. numerary is to the standard numbers module as beartype.typing is to the standard typing module – sort of. Okay, not really. But let's just roll with this awkward analogy. So, beartype.numbers would basically be the supercharged runtime-friendly numeric typing API that everyone always wanted but only @posita was brave enough to chain his immaculate reputation, GitHub karma, and lasting legacy to.
  • Alternately, just stuff everything into beartype.typing. I'd kinda prefer to maintain a one-to-one mapping with typing, but I can also see the argument for a monolithic typing API that just does everything.

You make the call. I'll roll the dice and tackle the issue tracker uproar.

@posita posita deleted the posita/0/caching-protocol branch March 2, 2022 14:31
posita added a commit to beartype/numerary that referenced this pull request Mar 5, 2022
... and separate override functionality from caching functionality. This will be useful
if beartype/beartype#86 lands and we decide to use that version (somehow) instead of
replicate our own.
posita added a commit to beartype/numerary that referenced this pull request Mar 5, 2022
Now that beartype/beartype#86 has landed (and made it into a release), we can remove our own base implementation and use that one instead. We still provide our own implementation that allows overriding runtime checking, but it neatly derives from ``beartype``'s.
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 this pull request may close these issues.

Help numerary help @beartype (help numerary) by porting CachingProtocolMeta to beartype
3 participants