-
Notifications
You must be signed in to change notification settings - Fork 180
hdl.ast: implement ShapeCastable.__subclasshook__. #830
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really impressed with you figuring out how to do this so quickly, I don't think I'd have managed with the amount of Python bugs involved.
amaranth/hdl/ast.py
Outdated
# precisely because ABCMeta.__subclasscheck__ will, in a | ||
# non-early-returning case, end up recursing on every subclass of | ||
# ShapeCastable, including lib.enum.EnumMeta and | ||
# lib.data._AggregateMeta. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is so cursed.
amaranth/hdl/ast.py
Outdated
@@ -110,7 +145,7 @@ def cast(obj, *, src_loc_at=0): | |||
while True: | |||
if isinstance(obj, Shape): | |||
return obj | |||
elif isinstance(obj, ShapeCastable): | |||
elif ShapeCastable in obj.__class__.__mro__: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the only part of the PR/fix that gives me pause--this part is kind of ... Extremely obscure. Is there any better way to make this check? I was hoping that overriding __instancecheck__
but not __subclasscheck__
would be a viable solution, but having thought about it more that seems even worse than this.
I can see having to just accept this but it doesn't have to make me happy :s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re: the type(obj).mro()
suggestion (per @mwkmwkmwk)—this is almost OK, but unfortunately still not:
>>> from amaranth.lib.enum import Enum as AmaranthEnum
>>> class EnumA(AmaranthEnum): X = 0
...
>>> EnumA.__class__.__mro__
(<class 'amaranth.lib.enum.EnumMeta'>, <class 'amaranth.hdl.ast.ShapeCastable'>, <class 'enum.EnumType'>, <class 'type'>, <class 'object'>)
>>> type(EnumA).mro()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method type.mro() needs an argument
>>>
mro()
is defined on classes, not metaclasses: i.e. it's an instance method on type
. For all call-sites, where the object we're checking may have a metaclass as its type()
—which includes passing any enum class—this will fail. All call-sites may receive enum classes.
(It's worth noting this behaviour isn't due to any interaction with ABCMeta
.)
We can use type(obj).__mro__
, which contains 50% fewer underscores than what the PR currently proposes, so that's a start. (I've also found one site where we can actually use isinstance
without causing recursive bad.)
One 'solution' is to make a helper and be done with it. The naming is awkward: ShapeCastable.actual_instance(o)
? _utils.in_type_mro(o, c)
? c in _utils.tmro(o)
?
Another is to reduce the number of places we have to do this to less than 3, and then it's under threshold for refactoring!
More thoughts below.
I was thinking of using
Oh, that's just an error on my part. Amaranth enums with no defined shape are intended to be a complete drop-in replacement for Python enums. IIRC the empty enum isn't shape-castable because of inheritance concerns? but due to the policy I mention that clearly must be fixed as a matter of principle even if there are consequences we have to deal with elsewhere. This also resolves objection 2b which is convenient. |
At the very least, it turns out adding our own We need ABCMeta's calls to Pros:
Cons:
|
Codecov Report
@@ Coverage Diff @@
## main #830 +/- ##
==========================================
+ Coverage 83.75% 83.85% +0.09%
==========================================
Files 54 54
Lines 7677 7704 +27
Branches 1867 1871 +4
==========================================
+ Hits 6430 6460 +30
+ Misses 1048 1045 -3
Partials 199 199
... and 1 file with indirect coverage changes 📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
Actually, 2b is kind of half-resolved already, and half-not-touched by this. Revisiting it:
It turns out But what does |
Continuing from thoughts in #830 (comment): There's a number of places where ISTM that we can clarify things: We can consider a different ABC for actual subclassing use—a terrible name would be An alternate solution proposes even bigger hacks where we magically insert a base class into subclasses of |
A proof of concept of the |
I've realized that, even if we abandon the idea of using I tried this. And you know what? It's so much less annoying. We have zero workarounds, except for implementing the abstract machinery ourselves. (But that sits in Will clean this up and push, but I think this is probably what the solution should look like, without a compelling reason to still want |
OK, I'm way happier with this. See 9b1aa95. I've included the " Turns out avoiding
I'm not sure how significant the latter may turn out to be. |
If we add a |
Hmmm, OK. Let's do this without breaking anything.
This doesn't seem that hacky or magical anymore. (Dropping ABCMeta and related hacks refunded quite a bit of magic budget.) It's quite straight-forward and means the diff to users is now "there's an extra type we can use to check specifically for the user-defined methods", without any loss or modifying existing code. All internal uses of One question about this approach is whether we export Currently just fixing up the coverage (/getting coverage diffs running locally); if this approach looks OK to you, I'll repeat for tl;dr:
|
Today seemed like a fine day to keep diving deep, so I explored the |
Noting: I feel like this does (or will) overlap fairly substantially with #778. |
Rebased this (and main...charlottia:amaranth:castable-protocols-valuecastable) post-RFC 22. |
There's a fair bit to work around here -- see comments.
We need to implement @AbstractMethod ourselves, and we miss out on ABC's caching, but that's it.
Deferring to the subclass-check heuristic for most cases wins us a modest performance benefit in most cases, but decreases the resolution of `isinstance` checks for ShapeCastable. The performance concern is if a CustomShapeCastable has an expensive `as_shape` implementation. This commit does attempt it, meaning a CustomShapeCastable instance that nonetheless fails `Shape.cast` will not be identified as a ShapeCastable instance.
Thank you for this work, @charlottia. We have decided to go with a new approach based on RFC 35, and this PR is now superseded by #983. |
Got it, thanks! |
This PR intends to eventually implement
ValueCastable.__subclasshook__
too. When it does so: fixes #753.This is a draft with just the
ShapeCastable
side of things working — as best I can tell — almost as well as we'd like. There might be some more cases to handle in the workarounds. What workarounds? See python/cpython#81062. This is a bit uglier than I'd like.ABCMeta
) would be easier in sum or not, but I plan to.__subclasshook__
doesn't get enough information to differentiate different enum subclasses. This fails objection (2b) from #753 (comment).Enum
is shape-castable? Unlike Amaranth's, they are—they cast tounsigned(0)
.