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
feat: adding math.TypeHint and is_subclass #136
Conversation
Brilliance personified. I cannot believe you bit-banged that out in a night. But, Harvard, so... I guess I can believe. You amaze even as you stun, @tlambert03. Welp. Let's get this
😮
...heh. Yeah. So, our test suite is obsessive-compulsively organized to guarantee that unit tests exercising lower-level functionality are run before unit tests exercising higher-level functionality. Wouldn't want to test the public Thankfully, nothing and nobody else actually use the Eventually, a reasonable landing place for that submodule would probably be within a new test subpackage living at:
Dissecting that:
Unrelatedly, I now sadly note that... Type Hints Are Only Partially OrderedOhnoes! I committed the cardinal sin of wishful thinking when I applied the Type hints almost constitute a total ordering under subtype compatibility; they satisfy reflexivity, transitivity, and antisymmetry as expected. Tragically for us, they violate totality (i.e., strongly connected). Bleakly consider: >>> from collections.abc import Awaitable
>>> TypeHint(list[int]) <= TypeHint(Awaitable[str])
False
>>> TypeHint(Awaitable[str]) <= TypeHint(list[int])
False Right? Okay. So that means type hints violate totality. If type hints satisfied totality, then either There's Probably a Point to All This?Shockingly, there is. Because type hints only constitute a partial ordering under subtype compatibility, we can't apply the
This should still mostly be trivial. Three of those can be readily defined in terms of the other three: e.g., def __ne__(self, other: TypeHint) -> bool:
return not self.__eq__(other)
def __gt__(self, other: TypeHint) -> bool:
return self.__ge__(other) and self.__ne__(other)
def __lt__(self, other: TypeHint) -> bool:
return self.__le__(other) and self.__ne__(other) That only leaves Annnnnyway... Your work is brilliance and these are conundrums for another evening. For tonight we play video games until dawn and dream bleary-eyed of a creepy AI that will someday code all this for us and submit PRs with our usernames on them. 🤖 |
Yeah, I've been having this thought as I work on it. Types are trees or graphs, not lines. So, strictly speaking, ordering will probably not make too much sense in most cases. Put another way, the same thing you showed above for In [1]: a = {1,2,3}
In [2]: b = {2,3,4}
In [3]: a <= b
Out[3]: False
In [4]: b <= a
Out[4]: False I don't think it makes a huge difference though. Ultimately, we're just using the |
This commit is the first in a commit chain improving our private `beartype._util.hint.pep.utilpepget.get_hint_pep_args()` getter to **deflatten** (i.e., restore to a facsimile of their original form) `Callable[...]` type hints passed to that getter, en-route to resolving issue #137 kindly submitted by Harvard microscopist and all-around typing math guru @tlambert03 (Talley Lambert), required as a mandatory dependency of pull request #136 also kindly submitted by @tlambert03. Specifically, this commit lightly refactors that getter in preparation for subsequent surgery as well as detailing internal improvements needed elsewhere for this to fully behave as expected. (*Long-tallied talons!*)
ok, this is getting a little closer to something I think I'd actually be able to use. plenty of tests are working, tuples, callables... (still need to do generators). I removed total ordering in favor of directly making set-like methods (but kept the ordered operators). I wonder if we might want to soften some of the comment language about total ordering to use more set-like language (maybe even the module name "math"?) But I'm still kinda just "doing shit until it mostly works". At your convenience, I'd appreciate a look at the general patterns. The comments are far from beartype standards! but mostly want to make sure we're not scratching our back on the wrong tree here 😂 At some point, I'll welcome you to take this over and "beartypify" it directly if you want to get it across the finish line! |
O frabjous day! Your reign of excellence continues, I see. Now, let's see...
Indeed, we can and we should do all those things! And I see that you have now done them. It's like you have a crystal ball into my brain, which is honestly kinda unsettling. 🔮 Relatedly, as a user convenience, we might also consider publicly exposing this convenience tester you've helpfully privatized at the top of def _is_subtype(subt: object, supert: object) -> bool:
return TypeHint(subt) <= TypeHint(supert) The # In "beartype.math._mathcls":
def is_subtype(subtype: object, supertype: object) -> bool:
'''
Insert docstring for great glory here!
'''
return TypeHint(subtype) <= TypeHint(supertype)
# In "beartype.math.__init__":
from beartype.math._mathcls import is_subtype
...excellent. Your prowess as an API designer precedes you. That's good.
Indeedy! Speaking of, would you mind globally replacing the substring Technically, the set of all possible type hints is a partially ordered countably infinite set. Pragmatically, Python doesn't do the whole "countably infinite set" thing out-of-the-box. It does do the whole "partially ordered" thing out-of-the-box. So, in the interest of clarity, I'd kinda prefer the latter if you don't mind too terribly much. Also, there are some really interesting well-known algorithms and data structures you can apply to partial orders. Topological sorts and network clustering sort of stuff. What could be funner? I mean, really. By highlighting that Okay. I'm not saying we should actually do this, but... we could augment Pinch me. I must be dreaming. Let's not do this in this PR. Possibly ever. Comments ContinueOkay. Let's get pragmatic, yo! Things we might want to do – possibly even in this PR:
def __hash__(self) -> int:
return hash(self._hint)
>>> import typing as t
# This is good.
>>> t.List[str] == t.List[str]
True
# This is less good.
>>> list[str] == t.List[str]
False An alternative approach would be for our class TypeHint(object):
...
def __eq__(self, other: object) -> Union[bool, NotImplemented]:
# If that object is *NOT* an instance of the same class, defer to the
# __eq__() method defined by the class of that object instead.
if not isinstance(other, TypeHint):
return NotImplemented
# Else, that object is an instance of the same class.
#FIXME: Maybe store as "self._hint_sign" to avoid lookups everywhere?
# Sign uniquely identifying this and that hint if any *OR* "None"
# (i.e., if either of these hints are *NOT* actually type hints).
self_sign = get_hint_pep_sign_or_none(self._hint)
other_sign = get_hint_pep_sign_or_none(other._hint)
# If either...
if (
# These hints have differing signs, these hints are unequal *OR*...
self_sign is not other_sign or
# These hints have a differing number of child type hints...
len(self._hints_child_ordered) != len(other._hints_child_ordered)
):
# Then these hints are unequal.
return False
# Else, these hints share the same sign and number of child type hints.
# Return true only if all child type hints of these hints are equal.
return all(
self._hints_child_ordered[hint_child_index] ==
other._hints_child_ordered[hint_child_index]
for hint_child_index in range(self._hints_child_ordered)
)
class _TypeHintClass(object):
...
def __eq__(self, other: object) -> Union[bool, NotImplemented]:
# If that object is *NOT* a partially ordered type hint, defer to the
# __eq__() method defined by the class of that object instead.
if not isinstance(other, TypeHint):
return NotImplemented
# Else, that object is a partially ordered type hint.
# Return true only if these hints describe the same class.
return isinstance(other, _TypeHintClass) and self._hint is other._hint
from beartype._util.cache.utilcachecall import callable_cached
class TypeHint(object):
...
@callable_cached
def __new__(cls, hint: object) -> "TypeHint": ... Given that, this constraint should now be true for any arbitrary type hint >>> TypeHint(hint) is TypeHint(true)
True
def __le__(self, other: object) -> bool:
"""Return true if self is a subtype of other."""
if not isinstance(other, TypeHint):
return NotImplemented
return self.is_subtype(other) le sigh.
def __repr__(self) -> str:
return f"TypeHint({repr(self._hint)})" And now for the big guns...
|
This commit is the first in a commit chain defining a new internal private API for introspecting ``Callable[...]`` type hints at runtime, required as a mandatory dependency of pull request #136 kindly submitted by Harvard microscopist and all-around typing math guru @tlambert03 (Talley Lambert). Specifically, this commit defines: * A new private `beartype._util.hint.pep.proposal.pep484585.utilpep484585callable` submodule. * In that submodule, a new `get_hint_pep484585_callable_args()` getter introspecting the parameter subtype hint subscripting (indexing) a ``Callable[...]`` type hint. (*Damaging imaging, dame!*)
This commit is the next in a commit chain defining a new internal private API for introspecting ``Callable[...]`` type hints at runtime, required as a mandatory dependency of pull request #136 kindly submitted by Harvard microscopist and all-around typing math guru @tlambert03 (Talley Lambert). Specifically, this commit defines a new private `beartype._util.hint.pep.proposal.pep484585.utilpep484585callable._die_unless_hint_pep484585_callable()` validator validating the passed object to be such a type hint as well as preliminary tests exercising this functionality. (*Insecure sinecure!*)
This commit is the next in a commit chain defining a new internal private API for introspecting ``Callable[...]`` type hints at runtime, required as a mandatory dependency of pull request #136 kindly submitted by Harvard microscopist and all-around typing math guru @tlambert03 (Talley Lambert). Specifically, this commit: * Generalizes our existing low-level private `beartype._util.hint.pep.utilpepget.get_hint_pep_sign()` getter to optionally accept `exception_cls` and `exception_prefix` parameters. * Improves our new higher-level private `_die_unless_hint_pep484585_callable()` validator to pass those parameters to that getter, unifying the exception types raised by that validator and simplifying unit tests of that validator. (*Robust robots or bust!*)
This commit is the next in a commit chain defining a new internal private API for introspecting ``Callable[...]`` type hints at runtime, required as a mandatory dependency of pull request #136 kindly submitted by Harvard microscopist and all-around typing math guru @tlambert03 (Talley Lambert). Specifically, this commit almost finalizes the draft implementation of our new high-level `get_hint_pep484585_callable_args()` getter, which currently only lacks support for `typing.ParamSpec[...]` and `typing.Concatenate[...]` child type hints. (*Actionable red redaction!*)
This commit is the next in a commit chain defining a new internal private API for introspecting ``Callable[...]`` type hints at runtime, required as a mandatory dependency of pull request #136 kindly submitted by Harvard microscopist and all-around typing math guru @tlambert03 (Talley Lambert). Specifically, this commit almost finalizes the draft implementation of our new high-level `get_hint_pep484585_callable_args()` getter with support for `typing.ParamSpec[...]` and `typing.Concatenate[...]` child type hints. Further refactoring is warranted for collective sanity. (*Irreducible dulcimer!*)
This commit by Harvard microscopist and general genius @tlambert03
Thanks so much to @tlambert03 for his phenomenal work here. |
💥 💥 💥 |
Yay! Our test suite once again passes – except for
Silence, |
hooray! Thanks for all the comments and tips... Even though you generously merged anyway, I'll follow up with some improvements based on these suggestions |
You're most welcome – and thanks yet again for this Big Bump to @beartype. 🤗 Please punch my avatar if I accidentally stomp on your parallel efforts, but I'll be massaging the
Oh, and I just stumbled over @napari on my way to GitHub work today. I am bedazzled. If @napari isn't the most impressive biomedical-oriented 3D Python viewer evar, I will livestream our Maine Coon eating my Tilley Hat sprinkled with catnip. Permissive licensing is only the cherry on top of your world-class crêpe. Educated prediction: @napari dominates client-side 3D visualization in 2022. Just... wowza. 😮 |
This commit is the next in a commit chain repairing currently failing tests pertaining to the `beartype.door` subpackage, recently implemented by Harvard microscopist and general genius @tlambert03 in PR #136. Specifically, this commit: * Renames `beartype.math` to `beartype.door`. N-n-now... hear me out here. I 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. * Refactored `beartype_test.test_is_subtype` to defer unsafe instantiation of non-trivial global variables to test-time via `pytest` fixtures. * Shifted `beartype_test.test_is_subtype` to `beartype_test.a00_unit.a60_api.door.test_door` for disambiguity and testability. Since `beartype.door` remains critically broken under both Python 3.7 and 3.8, tests remain disabled until the Typing Doctor gets in. (*Hazardous hazmats are arduous!*)
Catch! 🏀 If you'd like, the ball's in your court. Tests still fail under Python 3.7 and 3.8, but we're considerably closer. When you get a free minute, ...so never is what I'm saying would you mind taking a look at the failing I may have broken something with all of my shifty behaviour. If so, I profusely apologize. If not, I spit into the wind! Still, something smells disgustingly fishy there. It might be me. But it might not be me. |
This commit is the next in a commit chain repairing currently failing tests pertaining to the `beartype.door` subpackage, recently implemented by Harvard microscopist and general genius @tlambert03 in PR #136. Specifically, this commit conditionally ignores `beartype.door` tests under Python < 3.9 -- ensuring these tests at least continue to run under Python >= 3.9. (*Formidable formulations of maidenless middens!*)
I can confirm that the Until we can decipher what exactly went wrong under older Python versions, our test suite now conditionally ignores that test under Python < 3.9 – ensuring tests continue to run under at least Python ≥ 3.9. Let's see what insights a debauched Friday evening brings. |
Test party continues. Gods, but I hope the answer is: "Go to sleep, you." |
This commit is the next in a commit chain repairing currently failing tests pertaining to the `beartype.door` subpackage, recently implemented by Harvard microscopist and general genius @tlambert03 in PR #136. Specifically, this commit: * Restores the `test_typehint_new()` unit test, which now successfully passes on all Python versions. * Conditionally ignores the `test_typehint_equals()` unit test, which continues failing under Python < 3.9 -- ensuring this test at least continues to run under Python >= 3.9. (*Unindented dentition of dendritic orientations!*)
This commit is the next in a commit chain repairing currently failing tests pertaining to the `beartype.door` subpackage, recently implemented by Harvard microscopist and general genius @tlambert03 in PR #136. Specifically, this commit: * Declares a new `beartype.roar.BeartypeDoorNonpepException` type, raised when the `beartype.roar.TypeHint` constructor is passed an object that is *not* a PEP-compliant type hint currently supported by the DOOR API. * Extricates the `beartype.door._doorcls.is_subclass()` and `beartype.door._doorcls.die_unless_typehint()` functions into a new `beartype.door._doortest` submodule for maintainability. * Folds the prior `test_typehint_fail()` unit test into the existing `test_typehint_new()` unit test. (*Thunderous blunderbuss!*)
This commit is the next in a commit chain repairing currently failing tests pertaining to the `beartype.door` subpackage, recently implemented by Harvard microscopist and general genius @tlambert03 in PR #136. Specifically, this commit: * Resolves all pending `pyright` complaints concerning the `beartype.door` subpackage. Thankfully, `pyright` now *finally* passes across all supported Python versions. * Extricates the `HINT_SIGNS_ORIGIN_ISINSTANCEABLE_ARGS_*` family of sign sets into the more suitable `beartype._data.hint.pep.sign.datapepsignset` submodule. * Extricates the unit test exercising those sets into a more suitable new `beartype_test.a00_util.a00_data.hint.pep.sign.test_datapepsignset` test submodule. (*Suitable suit table!*)
API banging continues. I'd just like to reiterate, @tlambert03, that your work here was sufficiently phenomenal that it convinced someone they were wrong. That someone is me. How often does that happen irl? Less often than we all would like. So, what was I wrong about? I was wrong to neglect the object-oriented approach. Instead, I implemented @beartype 100% imperatively. Sure, there are a handful of dataclasses percolating about – but the code generation API at the heart of @beartype is almost purely procedural. That's good, in the sense that that's as wickedly performant as pure-Python can be. But that's also bad, in the sense that that's increasingly unusable, unreadable, and unmaintainable. I'll take a maintainable decorator over a performant decorator. The Future Beckons with a Creepy White GloveEnter your API, stage left. I'm dumbfounded at how blatantly superior this API is to everything else I invented. It's Pythonic, it's usable, it's readable, it's maintainable, and it's on the cusp of just working. What I'm trying to say here is that @beartype will eventually replace everything it internally does with this API instead. To do so, I'll be slowly building out support for efficient This will probably take a year or two. But I'm committed to making this happen, because this has to happen. Sanity demands a sane API. Thus spake the Bear. 🐻 |
This commit is the next in a commit chain repairing currently failing tests pertaining to the `beartype.door` subpackage, recently implemented by Harvard microscopist and general genius @tlambert03 in PR #136. Specifically, this commit trivially documents various internal refactorings to be subsequently applied to our private `beartype.door._doorclass._TypeHintCallable` subclass. In other words, we did nothing meaningful and have no regrets. (*Lucky pluckiness nestled in a bested duck!*)
wowie! that's big news. I'm glad to hear you're finding some inspiration leaking out of this PR into other areas. I agree, this pattern did end up feeling maintainable and relatively easy to reason about (breaking up the recursion into little digestible bits). ... and I guess it's not a huge leap to just kinda add an Performance is nothing to shrug off though, so I look forward to seeing what your uncompromising nature comes up with as an ideal marriage of the two :) |
Sadness ensues, because Python 3.7 and 3.8 are still failing. I'm currently re-enabling each test in the If you find a spare weekend between this and your other open-source commitments, napari 😮 napari it'd be super-swell if you could investigate the But at least |
will take a look! |
...heh. Our policy is simple. Exactly as you surmise, we gratefully appreciate the existence of Currently, this means:
All other Let us now explain why. Historically, PEP authors have tended to trial-run their unfinished beta implementations of upcoming type hint factories (under peer review by the We see where this is going. It's pure anarchy in the streets! Most of
Please God(s), don't test the Oh – and thanks so much for whatever generous assistance you're able to render. Your time is valuable. Your plate is full. We happily accept whatever you hurl at us! |
This commit is the next in a commit chain repairing currently failing tests pertaining to the `beartype.door` subpackage, recently implemented by Harvard microscopist and general genius @tlambert03 in PR #136. Specifically, this commit trivially documents yet more internal refactorings to be subsequently applied to our private `beartype.door._doorclass._TypeHintCallable` subclass. In other words, we still did nothing meaningful and still have no regrets. (*Fair-weather leather-feathered friends!*)
ok, the issue with python 3.7 and 3.8 appears is somewhat related to a difference between from It appears that back in 37 and 38, the default argument to a bare, unparametrized generic like what it means for usI can make all tests in beartype/beartype/_util/hint/pep/utilpepget.py Lines 90 to 94 in 6582be5
to read as: if getattr(hint, '_special', False):
return ()
return getattr(hint, '__args__', ()) unfortunately, that then breaks some of your other tests I'm not really sure how you want to fix this one... here's the options I see:
thoughts? |
Breathtaking writeup! You continue to both stun and impress. The answer, of course, is 2. Right? You knew that was coming. @beartype is here to do things semantically right, even when that means doing things syntactically wrong. Moreover, we really have to keep doing that. I'd never actually noticed that our current approach prevented us from distinguishing an unsubscripted Thankfully, you even solved that the Right Way™ too. Let's temporarily break tests by implementing your proposed solution ala:
Does that sound reasonable? Honestly, I can't thank you enough – but I will anyway. 👏 🥳 👏 |
Callooh! Callay! I'm fairly sure (but technically uncertain) that tests that fail after applying your wondrous fix can be trivially repaired by:
# Let's change this line...
_IS_ARGS_HIDDEN = _IS_TYPEVARS_HIDDEN
# ...to resemble this instead. Sanity, we restore thee.
_IS_ARGS_HIDDEN = False That's it. I think? Let's see how far the rabbit hole goes. 🕳️ 🐇 |
This commit by Harvard microscopist and general genius @tlambert03 is the next in a commit chain repairing currently failing tests pertaining to the `beartype.door` subpackage, recently implemented by... yup, it's @tlambert03 strikes again in PR #136. Specifically, this commit improves coverage by extending unit tests for this subpackage to additionally test PEP 484- and 585-compliant `{collections.abc,typing}.Type[...]` type hints against the `beartype.door.TypeHint` API. (*Long-lasting bold holdfast!*)
closes #133
lots of things to be cleaned-up, documented, and discussed here. But opening a PR to discuss progress on this.
You can see currently passing checks in test_is_subtype (not sure where I should be putting the test in the suite).
Will ping you with more specific questions/topics when ready