Repository navigation
Accept inferred explicit-Any variable as base class - #22037
Dextheking1 wants to merge 23 commits into
Conversation
Fixes python#21998: mypy rejected subclassing a variable whose type is inferred as Any (e.g. `x = args[1]` where `args: *tuple[int, Any]`), even though an explicitly `Any`-annotated variable is accepted as a base class. Semantic analysis runs before type inference, so a base class that is a variable with an uninferred type cannot be validated there. Such bases are now provisionally treated as Any and validated by the type checker once the type is known (TypeChecker.check_deferred_base_classes). An inferred `Any` is accepted only if it comes from an explicit annotation (TypeOfAny.explicit); `Any` leaking in from unannotated code is still rejected, preserving existing behavior (e.g. dynamic class plugin negatives). `--disallow-subclassing-any` is honored for inferred-Any bases as well.
…d-any-base-class Resolved a content conflict in test-data/unit/check-classes.test: both this branch and upstream added the testPartialNoneTypeClassMethod case at the end of the file; kept upstream's copy and this branch's two new test cases.
for more information, see https://pre-commit.ci
The previous push accidentally deleted mypy/checker.py, mypy/nodes.py, mypy/semanal.py and mypy/typeanal.py, breaking every CI job. Restore them byte-identical from upstream master (8ddae78). The regression tests for python#21998 are kept; the underlying fix still needs to be re-implemented against the current layout.
for more information, see https://pre-commit.ci
The merge in 55d210e deleted mypy/checker.py, mypy/nodes.py, mypy/semanal.py and mypy/typeanal.py; the follow-up restore brought back plain upstream copies, dropping the python#21998 implementation (DeferredBaseClassVar deferral in semanal/typeanal, deferred_base_classes on TypeInfo, and TypeChecker.check_deferred_base_classes / is_explicit_any). Re-apply the implementation from c56f103 on top of the current upstream files. Verified locally (Hatch VM): testSubclassInferredAnyVariable and testSubclassInferredNonAnyVariable pass, and the full check-classes.test suite is green (612 passed, 1 xfailed).
This comment has been minimized.
This comment has been minimized.
The mypyc-compiled CI jobs build mypy with its own type checker, which flagged three errors in the new code: redundant get_proper_type() on AnyType.source_any and TypeType.item (both already ProperType), and is_explicit_any() called with ProperType | None from var.type. Drop the redundant calls and narrow out None before the is_any check. No behavior change: get_proper_type is identity on proper types, and None still falls through to the deferred-error branch. Verified: mypy self-check on checker.py is clean; testSubclassInferredAnyVariable and testSubclassInferredNonAnyVariable pass.
This comment has been minimized.
This comment has been minimized.
|
Can you add the test cases discussed in #22002 ? Thanks so much in advance! (You don't have to produce the same output chosen by that author for each case but it would be useful to have expected output asserted for all the examples covered there.) |
|
Done, added all eight cases from #22002 with expected output asserted against this implementation. Direct index expressions like |
Covers the remaining negative direction of the deferred base-class check: an inferred Any that does not come from an explicit annotation is still rejected as a base class.
|
Done. Added the #22002 examples as test cases in check-classes.test: tuple indexes, *args forms (plain, unpacked, nested, unbounded), Sequence indexing, plus annotated and unannotated locals. Direct index expressions like |
This comment has been minimized.
This comment has been minimized.
|
I will review this soon but can you update your PR so that it passes the mypy_primer tool also? E.g. #22002 did not break any existing projects whereas it appears the mypy_primer tool indicates this change would break scipy, pandas, and other major projects, which is not an expected side effect. |
…: mypy/checker.py)
…: mypy/semanal.py)
…: mypy/typeanal.py)
…: test-data/unit/check-classes.test)
|
Thanks for running primer. I dug into the new errors and found a real regression: the deferred base class check was reporting in unchecked functions and inside Literal[...], where semantic analysis silently drops these errors. That accounts for the scipy, pandas, zope.interface, and setuptools reports, which are all dynamic bases in unannotated code. I pushed a fix that makes the checker skip those positions, matching what semanal did before, plus regression tests. The kornia INTERNAL ERROR is a sqlite "database is locked" in the primer runner's cache initialization, unrelated to this change. The prefect and steam.py diffs look like different error messages on code that already errored on master rather than new breakage, but I have not had a chance to run primer myself to confirm the full output is clean. |
for more information, see https://pre-commit.ci
This comment has been minimized.
This comment has been minimized.
dibrinsofor
left a comment
There was a problem hiding this comment.
Looks good to me, sans a couple nits.
| and typ.source_any is not None | ||
| ): | ||
| typ = typ.source_any | ||
| return isinstance(typ, AnyType) and typ.type_of_any != TypeOfAny.unannotated |
There was a problem hiding this comment.
can we build this up using is_unannotated_any and get_original_any
There was a problem hiding this comment.
You might also want to check against TypeOfAny.explicit if truly we only want user defined Anys
| # The type checker validates them once the types are known (see | ||
| # TypeChecker.check_deferred_base_classes). This is not serialized; it | ||
| # is only meaningful within a single build. | ||
| self.deferred_base_classes: list[tuple[Var, Expression, bool]] = [] |
There was a problem hiding this comment.
could we make a result/dataclass (w named params) for the pending validation instead of the long tuple?
for more information, see https://pre-commit.ci
This comment has been minimized.
This comment has been minimized.
willy-b
left a comment
There was a problem hiding this comment.
Thanks very much for your contribution!
(To be clear to outside readers, (1) I am not on the MyPy team, just mostly a bug reporter, (2) I have never been in contact with you before nor did you discuss the upstream bug I reported with me before sending this, which is fine in open source but just for context.
I was originally thinking to offer to send my own solution after confirming all aspects reported were bugs in the original ticket #21998 with the mypy team (based on mypy behaving differently than other mainstream Python typecheckers like pyright 1.1.414 and pyrefly 1.3.1 and seemingly inconsistent with itself), if the team did not have time. However, I think @ilevkivskyi is working on this area fwiw and will likely get to it as he e.g. recently landed a PR with something very close to what I was testing in my fork to fix the mypy crash I discovered and reported earlier at #21907 (the mypy tool itself told me to report that crash) instead of asking me to submit that change directly.)
Note, the team has not fully acked all aspects of the upstream issue (though my report is based on mypy behaving differently than other mainstream Python typecheckers like pyright 1.1.414 and pyrefly 1.3.1), making it a bit risky to work on this as the scope is not agreed upon. (They have also not fixed the cases mentioned in the upstream yet despite many related changes, I just checked.)
That said, it seems the intention of my issue "mypy rejects subclassing Any typed *args entries within a function" #21998 was misunderstood by you based on the asserted/expected behaviors you are checking in your unit tests. I mention these instead of focusing on your code first because ANY CODE consistent with these tests would be by definition accomplishing the wrong (even opposite) objective imho as the issue author of "mypy rejects subclassing Any typed *args entries within a function" #21998 , see my detailed inline comments below.
Out of curiosity, can you comment as to whether or not you used AI / LLM tools in working on this, and if so, which?
(E.g. I recently reviewed #22002 for this issue which was evidently Claude (Anthropic) assisted and for a different issue the seemingly Codex generated MyPy # 21908 (not linking to avoid tagging it as it is for a different problem), but this seems different than those.)
Thanks!
| def from_star_any(*args: Any) -> None: | ||
| class Sub(args[0]): # E: Variable "args" is not valid as a type \ |
There was a problem hiding this comment.
The goal of mypy rejects subclassing Any typed *args entries within a function #21998 is to in fact support the case you are asserting here will fail.
(Furthermore, you put the from_star_any(*args: Any) example into a testcase named testSubclassAnyFromIndexedTuple (which is correct for the preceding function but this should be in a different case).)
This should be supported like the from_any_param case you have below.
There was a problem hiding this comment.
@Dextheking1 can you mark which comments from #22037 (review) you believe you have resolved or explain why you are not resolving them? As far as I can tell you have not addressed these concerns when updating the PR today, thanks.
| def from_unpacked(*args: *tuple[int, *tuple[int, ...], Any]) -> None: | ||
| reveal_type(args[-1]) # N: Revealed type is "Any" | ||
| class Sub(args[-1]): # E: Variable "args" is not valid as a type \ |
There was a problem hiding this comment.
Thanks for copying this case I asked for in #22002 , however you copied the name and form with the opposite result of what I asked for there and in the issue you have attached this PR to.
The issue I reported in "mypy rejects subclassing Any typed *args entries within a function" #21998 includes as something to be fixed that this case is not supported (you are in fact asserting the type of args[-1] is Any but then asserting it cannot be subclassed (which is unexpected and generally inconsistent with other typecheckers like pyright 1.1.414 and pyrefly 1.3.1 or mypy in other cases, see the discussion at #5865 or examples in the parent issue #21998 or even your own example above from_any_param.
| def f(*args: *tuple[int, Any]) -> Any: | ||
| x = args[1] | ||
| class Sub(x): pass |
There was a problem hiding this comment.
Thanks for supporting this case (though I will note this is the one case I mentioned in the upstream ticket, where the type is inferred through an intermediate variable, where I am UNSURE the mypy team considers it a bug and am waiting for feedback).
However, see my other comments as using args[1] directly as a superclass like Sub(args[1]) should also be supported but you are in fact asserting the opposite (that is what clearly seems to be a bug but is not fixed by you here).
| def before_the_unpack(*args: *tuple[Any, *tuple[int, ...], int]) -> None: | ||
| class Sub(args[0]): # E: Variable "args" is not valid as a type \ |
There was a problem hiding this comment.
Thanks for copying this case from #22002 as requested, however you copied the setup but with the opposite result of what I asked for there and in the issue you have attached this PR to.
| def inside_the_unpack(*args: *tuple[int, *tuple[Any, ...], int]) -> None: | ||
| class Sub(args[1]): # E: Variable "args" is not valid as a type \ |
There was a problem hiding this comment.
Thanks for copying this case from #22002 as requested, however you copied the setup but with the opposite result of what I asked for there and in the issue you have attached this PR to.
There was a problem hiding this comment.
@Dextheking1 per your latest code in this one case (out of several I asked you about earlier at #22037 (review) ) it does make sense to me why you want to in some cases treat the inside the unpack as ambiguous since *tuple[Any, ...] stands for zero or more (not one or more) Any's per https://web.archive.org/web/20260918002706/https://typing.python.org/en/latest/spec/generics.html#:~:text=zero%20or%20more%20str%20values,%20and%20a%20str%20at%20the%20end ,
and I do agree with you that this one case per the spec and reveal_type is really a Any | int instead of Any. However, do note that one of the other major typecheckers pyrefly (1.4.0) passes this example (not rejects as you propose), so we may want to get a MyPy team member's input on the expected behavior. Thank you for fixing all the other cases I mentioned.
To clarify the example I suggest adding an assertion on the reveal_type behavior:
def inside_the_unpack(*args: *tuple[int, *tuple[Any, ...], int]) -> None:
reveal_type(args[1]) # N: Revealed type is "Any | builtins.int"
class Sub(args[1]): # E: Variable "args" is not valid as a type \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \
# E: Invalid base class "args" \
# E: Invalid type: try using Literal[1] instead?
pass
Also as I mention in another comment, the corresponding unambiguous, passing case of one or more Any instead of zero or more, should probably be added:
def across_or_inside_the_unpack_one_or_more_any(*args: *tuple[int, *tuple[Any, ...], Any, int]) -> None:
reveal_type(args[1]) # N: Revealed type is "Any"
class Sub(args[1]):
pass
| def from_unpacked(*args: *tuple[Any, ...]) -> None: | ||
| reveal_type(args[0]) # N: Revealed type is "Any" | ||
| reveal_type(args[1]) # N: Revealed type is "Any" | ||
| class Sub(args[0]): # E: Variable "args" is not valid as a type \ |
There was a problem hiding this comment.
Thanks for copying this case I asked for in #22002 , however you copied the name and form with the opposite result of what I asked for there and in the issue you have attached this PR to.
The issue I reported in "mypy rejects subclassing Any typed *args entries within a function" #21998 includes as something to be fixed that this case is not supported (you are in fact asserting the type of args[0] is Any but then asserting it cannot be subclassed (which is unexpected and generally inconsistent with other typecheckers like pyright 1.1.414 and pyrefly 1.3.1 or mypy in other cases, see the discussion at #5865 or examples in the parent issue #21998 or even your own example above from_any_param.
| def from_unpacked(*args: *tuple[int, Any]) -> None: | ||
| class Sub(args[1]): # E: Variable "args" is not valid as a type \ |
There was a problem hiding this comment.
The goal of mypy rejects subclassing Any typed *args entries within a function #21998 is to in fact support the case you are asserting here will fail.
| def from_unpacked(*args: *tuple[int, *tuple[Any]]) -> None: | ||
| reveal_type(args) # N: Revealed type is "tuple[builtins.int, Any]" | ||
| class Sub(args[1]): # E: Variable "args" is not valid as a type \ |
There was a problem hiding this comment.
Thanks for copying this case from #22002 as requested, however you copied the setup but with the opposite result of what I asked for there and in the issue you have attached this PR to.
The issue I reported in "mypy rejects subclassing Any typed *args entries within a function" #21998 includes as something to be fixed that this case is not supported (you are in fact asserting the type of args[1] is Any but then asserting it cannot be subclassed (which is unexpected and generally inconsistent with other typecheckers like pyright 1.1.414 and pyrefly 1.3.1 or mypy in other cases, see the discussion at #5865 or examples in the parent issue #21998 or even your own example above from_any_param.
| def from_unpacked2(*args: *tuple[*tuple[Any, ...]]) -> None: | ||
| reveal_type(args[0]) # N: Revealed type is "Any" | ||
| reveal_type(args[1]) # N: Revealed type is "Any" | ||
| class Sub(args[0]): # E: Variable "args" is not valid as a type \ |
There was a problem hiding this comment.
Thanks for copying this case I asked for in #22002 , however you copied the setup with the opposite result of what I asked for there and in the issue you have attached this PR to.
The issue I reported in "mypy rejects subclassing Any typed *args entries within a function" #21998 includes as something to be fixed that this case is not supported (you are in fact asserting the type of args[0] is Any but then asserting it cannot be subclassed (which is unexpected and generally inconsistent with other typecheckers like pyright 1.1.414 and pyrefly 1.3.1 or mypy in other cases, see the discussion at #5865 or examples in the parent issue #21998 or even your own example above from_any_param.
|
(P.S., I did not use any AI/LLM assistance in reviewing this -- I have not, across any of my Github PR reviews. |
This comment has been minimized.
This comment has been minimized.
|
Yes, I used AI assistance and I used my own AI agent. |
This comment has been minimized.
This comment has been minimized.
|
Went through all 9 comments. The tests asserted the wrong outcome, you were right. The checker now validates subscripted variable bases the same way it validates plain variables, so the Any cases below pass for real, not just on paper. Resolved:
Not flipped:
The negative cases (int and str items) keep the same errors as before. |
willy-b
left a comment
There was a problem hiding this comment.
Thanks again for your contribution and for addressing the mismatched expected behaviors I had brought up in #22037 (review) (except for one case where you maintain to deviate from pyrefly and I agree with you that the competing typechecker pyrefly might have it wrong and so just ask you add a reveal_type assertion to that test case to clarify why you expect it to be rejected).
Here is some more feedback, mostly on the test cases to make sure we agree on the expected behavior and handle all the cases before critiquing the implementation.
Remaining behavior level issues I see are e.g. lack of support for ambiguous type cases where the ambiguity is safe like builtins.type | Any (both type and Any should be able to be subclassed, so their union should be safe as well),
and lack of support for IndexExprs like [-(-1)] (should behave the same as [1]) .
Thanks!
| reveal_type(args[1]) # N: Revealed type is "Any" | ||
| class Sub(args[0]): | ||
| pass | ||
| class Sub2(args[1]): |
There was a problem hiding this comment.
You can add such tests anywhere in here, but this is a pretty good place to ensure your code is handling general "Expression" values and not implementing brittle handling for only the simple cases of IndexExpr being an IntExpr or an int primitive modified by a negative sign
E.g. update from_unpacked2 in the test case testSubclassAnyFromNestedUnpackedUnboundedStarArgsIndexInsideUnambiguous to add another Sub class using one of the args entries as its base, but using a slightly more complicated double negative expression as the IndexExpr:
def from_unpacked2(*args: *tuple[*tuple[Any, ...]]) -> None:
pass
class Sub2(args[1]):
pass
+ class Sub3(args[-(-1)]):
+ pass
Currently that fails but I would expect it to succeed.
| def index_expr_int_value(index: Expression) -> int | None: | ||
| """Value of a constant integer index expression, or None.""" | ||
| if isinstance(index, IntExpr): | ||
| return index.value | ||
| if isinstance(index, UnaryExpr) and index.op == "-" and isinstance(index.expr, IntExpr): | ||
| return -index.expr.value | ||
| return None |
There was a problem hiding this comment.
It seems you are implementing brittle handling for only the simple cases of IndexExpr being an IntExpr or an int primitive modified by a negative sign, instead of handling general Expressions.
Consider adding test cases where args[1] is type Any and confirming that e.g. args[-(-1)] also works in that case, for example by adding another 2 lines to your existing test testSubclassAnyFromNestedUnpackedUnboundedStarArgsIndexInsideUnambiguous to add another Sub class using one of the args entries as its base, but using a slightly more complicated double negative expression as the IndexExpr:
def from_unpacked2(*args: *tuple[*tuple[Any, ...]]) -> None:
pass
class Sub2(args[1]):
pass
+ class Sub3(args[-(-1)]):
+ pass
Currently your latest version fails such a check but I think we would expect such to succeed.
| def inside_the_unpack(*args: *tuple[int, *tuple[Any, ...], int]) -> None: | ||
| class Sub(args[1]): # E: Variable "args" is not valid as a type \ |
There was a problem hiding this comment.
@Dextheking1 per your latest code in this one case (out of several I asked you about earlier at #22037 (review) ) it does make sense to me why you want to in some cases treat the inside the unpack as ambiguous since *tuple[Any, ...] stands for zero or more (not one or more) Any's per https://web.archive.org/web/20260918002706/https://typing.python.org/en/latest/spec/generics.html#:~:text=zero%20or%20more%20str%20values,%20and%20a%20str%20at%20the%20end ,
and I do agree with you that this one case per the spec and reveal_type is really a Any | int instead of Any. However, do note that one of the other major typecheckers pyrefly (1.4.0) passes this example (not rejects as you propose), so we may want to get a MyPy team member's input on the expected behavior. Thank you for fixing all the other cases I mentioned.
To clarify the example I suggest adding an assertion on the reveal_type behavior:
def inside_the_unpack(*args: *tuple[int, *tuple[Any, ...], int]) -> None:
reveal_type(args[1]) # N: Revealed type is "Any | builtins.int"
class Sub(args[1]): # E: Variable "args" is not valid as a type \
# N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \
# E: Invalid base class "args" \
# E: Invalid type: try using Literal[1] instead?
pass
Also as I mention in another comment, the corresponding unambiguous, passing case of one or more Any instead of zero or more, should probably be added:
def across_or_inside_the_unpack_one_or_more_any(*args: *tuple[int, *tuple[Any, ...], Any, int]) -> None:
reveal_type(args[1]) # N: Revealed type is "Any"
class Sub(args[1]):
pass
| def f(y: Sequence[Any]) -> None: | ||
| class A(y[0]): pass |
There was a problem hiding this comment.
Thanks for supporting and adding this case I requested in the other PR at #22002 (comment) .
| def before_the_unpack(*args: *tuple[Any, *tuple[int, ...], int]) -> None: | ||
| class Sub(args[0]): | ||
| pass |
There was a problem hiding this comment.
nit: can you also add the corresponding after_the_unpack test case for coverage?
def after_the_unpack(*args: *tuple[int, *tuple[int, ...], Any]) -> None:
class Sub(args[-1]):
pass
Also, I am not sure if these two (before_the_unpack, after_the_unpack) belong in the test case you have named testSubclassAnyFromUnpackedStarArgsAmbiguousIndex as these are unambiguous index cases I think.
| def across_the_unpack(*args: *tuple[int, *tuple[Any, ...], int]) -> None: | ||
| class Sub(args[-2]): # E: Variable "args" is not valid as a type \ | ||
| # N: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases \ | ||
| # E: Invalid base class "args" \ | ||
| # E: Invalid type: try using Literal[-2] instead? | ||
| pass |
There was a problem hiding this comment.
There are two related cases that are expected to pass that are probably worth including to have coverage and catch regressions:
def across_or_inside_the_unpack_one_or_more_any(*args: *tuple[int, *tuple[Any, ...], Any, int]) -> None:
reveal_type(args[1]) # N: Revealed type is "Any"
class Sub(args[1]):
pass
def across_or_inside_the_unpack_one_or_more_any2(*args: *tuple[int, Any, *tuple[Any, ...], int]) -> None:
reveal_type(args[-2]) # N: Revealed type is "Any"
class Sub(args[-2]):
pass
(Note the above cases I suggest are NOT yet passing on MyPy master, but would in this branch. In the upstream/master they would show the types as Any but not allow them to be used as base classes.)
Also can you add reveal_type assertions before your asserted errors on subclassing? That way the questions of the types and whether the types are supported as base classes are separated.
There was a problem hiding this comment.
Resolved in 4e6dc32 ; before_the_unpack and after_the_unpack did not have the reveal_type added but they are obvious cases, so no pressure from me.
There was a problem hiding this comment.
added the reveal_type lines to before_the_unpack and after_the_unpack too
| if all(isinstance(item, AnyType) for item in possible): | ||
| return possible[0] |
There was a problem hiding this comment.
What about if there are a mix of possible type (or Any) instances? That should also be supported, right?
E.g. I think the following should pass (reveal_type can be omitted in the below but I include it to help explain why it should pass):
def across_the_unpack_ambiguous_with_type_should_be_fine(*args: *tuple[type, *tuple[Any, ...], int]) -> None:
reveal_type(args[-2]) # N: Revealed type is "builtins.type | Any"
class Sub(args[-2]):
pass
def across_the_unpack_ambiguous_with_type_should_be_fine2(*args: *tuple[int, *tuple[Any, ...], type]) -> None:
reveal_type(args[1]) # N: Revealed type is "Any | builtins.type"
class Sub(args[1]):
pass
(Note builtins.type might be just type in e.g. the MyPy Playground, it depends on the test configuration; only affects the reveal_type step which is just for clarity, even without those lines, the class Sub lines should pass.)
Noting that in general variables of type type should be able to be subclassed, e.g.
https://mypy-play.net/?mypy=2.4.0&python=3.12&gist=e92cca9c89d683d7b8a93e6640826ead
# passes on MyPy 2.4.0
# derived from example given in https://github.com/python/mypy/issues/5865#issuecomment-489446397 which they fixed to pass
def subclass(a_type: type) -> type:
class A(a_type): pass
return A
So given that type can be subclassed and Any can be subclassed, type | Any should be able to be subclassed, or type_a | type_b | Any or just type_a | type_b.
…rally tuple_item_at_index now returns the union of candidate items for an ambiguous index instead of None, and deferred base validation accepts a union whose items are all acceptable (explicit Any or a type object), so type | Any bases pass while Any | int still errors. index_expr_int_value now uses constant_fold_expr, so indexes like -(-1) work instead of only literal ints and single negations. Tests: Sub3 with args[-(-1)], reveal_type assertions on the ambiguous error cases, before/after_the_unpack moved to an unambiguous case, one-or-more Any cases, and the two type | Any cases.
|
All addressed in 4e6dc32:
Ran the full check-classes file locally: 631 passed. Self-check on checker.py is clean. |
for more information, see https://pre-commit.ci
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Thanks for today's updates to resolve most of the review comments so far. @Dextheking1 can you resolve the I am starting to review the implementation side now that the test cases are looking aligned with the ticket and passing, but this change should not have side effects on any existing base class usecases when adding support for retrieving base classes by index. |
A variable appearing as a type argument of a base class (e.g. the T in tuple[T]) was deferred as if it were the base class itself, and the checker then reported Invalid base class for it. Only defer when the variable is the base class expression itself (nesting level 0).
|
Fixed in 4297367. The deferral also fired for variables nested in a base class type argument (the T in tuple[T]), not just for the base class itself. Those keep the old error path now; only the base expression itself defers. Added a regression case (testSubclassVariableNestedInBaseClassArgs). Output on the prefect shapes matches master again, and the full testcheck suite passes locally. |
|
Diff from mypy_primer, showing the effect of this PR on open source code: scrapy (https://github.com/scrapy/scrapy)
+ tests/test_utils_deprecate.py:49: error: Unused "type: ignore[valid-type]" comment [unused-ignore]
+ tests/test_pipeline_crawl.py:277: error: Unused "type: ignore[valid-type]" comment [unused-ignore]
|
Fixes #21998.
mypy rejected subclassing a variable whose type is inferred as
Any(e.g.x = args[1]whereargs: *tuple[int, Any]), even though an explicitlyAny-annotated variable is accepted as a base class:Why it happened
Semantic analysis runs before type inference, so when a base class is a variable with an uninferred type,
TypeAnalysersawVar.type is Noneand reportedVariable ... is not valid as a type+Invalid base classprematurely.The fix
typeanal.py: newDeferredBaseClassVarexception, raised when a base-class variable has no inferred type yet (only on theallow_type_anypath, which is exclusive to base-class analysis).semanal.py:analyze_base_classescatches it, provisionally treats the base asAny(so MRO computation proceeds), and records(var, expr)on the new transientTypeInfo.deferred_base_classesfield. It also skips the premature--disallow-subclassing-anyerror for provisional bases.checker.py: newTypeChecker.check_deferred_base_classesvalidates each deferred base once the type is known. An inferredAnyis accepted only if it comes from an explicit annotation (TypeOfAny.explicit, unwrappingfrom_another_any);Anyleaking in from unannotated code is still rejected exactly as before, and non-Anyinferred types get the originalvalid-type/miscerrors.--disallow-subclassing-anyis honored.nodes.py: transient (non-serialized)TypeInfo.deferred_base_classesslot.test-data/unit/check-classes.test: newtestSubclassInferredAnyVariable(the issue repro) andtestSubclassInferredNonAnyVariable(negative case).Verification
mypy/test/testcheck.py: 8227 passed, 0 failed.testsemanal.py(577),testmerge.py(41), fine-grained class/base subset (141) all pass.blackandruffclean on changed files; changed files clean undermypy_self_check.ini(only pre-existing env-related notes remain).int/None/str/listbases still error identically;x: Any,x: type,*args: Anyelement, andMemberExpr(Holder.x) bases behave as before;--disallow-subclassing-anyerrors exactly once for inferred-Anyand not at all misleadingly for non-Any.