Skip to content

fix descriptor semantics for stub/interface declarations #3045#3151

Open
shuv-amp wants to merge 1 commit intofacebook:mainfrom
shuv-amp:fix/stub-descriptor-semantics-3045
Open

fix descriptor semantics for stub/interface declarations #3045#3151
shuv-amp wants to merge 1 commit intofacebook:mainfrom
shuv-amp:fix/stub-descriptor-semantics-3045

Conversation

@shuv-amp
Copy link
Copy Markdown

Summary

Fixes #3045

Stub and interface files can declare descriptor-backed attributes without showing the runtime initializer. Today we only apply descriptor semantics when the field is initialized in the class body, marked as ClassVar, or covered by a special case. That makes stub-declared descriptors fall back to their raw descriptor type instead of the __get__ result.

This change treats fields modeled as ClassFieldInitialization::Magic as class-level for descriptor detection. That restores descriptor semantics for stub and interface declarations while preserving the current behavior for annotation-only instance attributes in source files.

The regression test covers an inherited annotation-only descriptor declared in .pyi files.

Test Plan

  • python3 test.py --no-test --no-conformance --no-jsonschema
  • cargo test -p pyrefly descriptors --quiet
  • cargo test -p pyrefly test_foreign_key_basic --quiet

@meta-cla
Copy link
Copy Markdown

meta-cla bot commented Apr 15, 2026

Hi @shuv-amp!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@meta-cla
Copy link
Copy Markdown

meta-cla bot commented Apr 15, 2026

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@shuv-amp shuv-amp force-pushed the fix/stub-descriptor-semantics-3045 branch from 337c7e4 to f3cc153 Compare April 15, 2026 13:55
@github-actions github-actions bot added size/s and removed size/s labels Apr 15, 2026
)

Summary:
Stub and interface files can declare descriptor-backed attributes without
showing the runtime initializer. We only treated class-body assignments as
descriptor definitions, so stub-declared descriptors fell back to the raw
descriptor type instead of the `__get__` result.

Treat fields modeled as `ClassFieldInitialization::Magic` as class-level for
descriptor detection. This restores descriptor semantics for stub and interface
declarations while preserving the existing behavior for annotation-only
instance attributes in source files.

Add a regression test covering an inherited annotation-only descriptor in
`.pyi` files.
@shuv-amp shuv-amp force-pushed the fix/stub-descriptor-semantics-3045 branch from f3cc153 to e468758 Compare April 15, 2026 13:58
@github-actions github-actions bot added size/s and removed size/s labels Apr 15, 2026
@rchen152 rchen152 requested a review from stroxler April 15, 2026 16:41
@github-actions
Copy link
Copy Markdown

Diff from mypy_primer, showing the effect of this PR on open source code:

pip (https://github.com/pypa/pip)
- ERROR src/pip/_vendor/rich/_win32_console.py:384:39-46: Argument `_CField[c_short, int, c_short | int]` is not assignable to parameter `row` with type `int` in function `WindowsCoordinates.__new__` [bad-argument-type]
- ERROR src/pip/_vendor/rich/_win32_console.py:384:52-59: Argument `_CField[c_short, int, c_short | int]` is not assignable to parameter `col` with type `int` in function `WindowsCoordinates.__new__` [bad-argument-type]
- ERROR src/pip/_vendor/rich/_win32_console.py:394:39-52: Argument `_CField[c_short, int, c_short | int]` is not assignable to parameter `row` with type `int` in function `WindowsCoordinates.__new__` [bad-argument-type]
- ERROR src/pip/_vendor/rich/_win32_console.py:394:58-71: Argument `_CField[c_short, int, c_short | int]` is not assignable to parameter `col` with type `int` in function `WindowsCoordinates.__new__` [bad-argument-type]

django-stubs (https://github.com/typeddjango/django-stubs)
- ERROR django-stubs/contrib/contenttypes/fields.pyi:66:5-10: Class member `GenericRel.field` overrides parent class `ForeignObjectRel` in an inconsistent manner [bad-override]
- ERROR django-stubs/db/models/fields/files.pyi:52:5-10: Class member `FileDescriptor.field` overrides parent class `DeferredAttribute` in an inconsistent manner [bad-override]
+ ERROR django-stubs/db/models/fields/files.pyi:99:9-16: Class member `FileField.__get__` overrides parent class `Field` in an inconsistent manner [bad-override]
- ERROR django-stubs/db/models/fields/files.pyi:115:5-10: Class member `ImageFileDescriptor.field` overrides parent class `FileDescriptor` in an inconsistent manner [bad-override]
- ERROR django-stubs/db/models/fields/files.pyi:120:5-10: Class member `ImageFieldFile.field` overrides parent class `FieldFile` in an inconsistent manner [bad-override]
- ERROR django-stubs/db/models/fields/related_descriptors.pyi:22:5-10: Class member `ForeignKeyDeferredAttribute.field` overrides parent class `DeferredAttribute` in an inconsistent manner [bad-override]
- ERROR django-stubs/db/models/fields/reverse_related.pyi:95:5-10: Class member `ManyToOneRel.field` overrides parent class `ForeignObjectRel` in an inconsistent manner [bad-override]
- ERROR django-stubs/db/models/fields/reverse_related.pyi:115:5-10: Class member `OneToOneRel.field` overrides parent class `ManyToOneRel` in an inconsistent manner [bad-override]
+ ERROR django-stubs/db/models/fields/tuple_lookups.pyi:13:5-17: Class member `Tuple.output_field` overrides parent class `Func` in an inconsistent manner [bad-override]

asynq (https://github.com/quora/asynq)
- ERROR asynq/decorators.py:193:20-40: Object of class `DecoratorBase` has no attribute `asynq` [missing-attribute]
+ ERROR asynq/decorators.py:193:20-40: Object of class `DecoratorBinder` has no attribute `asynq` [missing-attribute]
- ERROR asynq/decorators.py:195:20-40: Object of class `DecoratorBase` has no attribute `asynq` [missing-attribute]
+ ERROR asynq/decorators.py:195:20-40: Object of class `DecoratorBinder` has no attribute `asynq` [missing-attribute]
- ERROR asynq/decorators.py:199:20-42: Object of class `DecoratorBase` has no attribute `asyncio` [missing-attribute]
+ ERROR asynq/decorators.py:199:20-42: Object of class `DecoratorBinder` has no attribute `asyncio` [missing-attribute]
- ERROR asynq/decorators.py:201:20-42: Object of class `DecoratorBase` has no attribute `asyncio` [missing-attribute]
+ ERROR asynq/decorators.py:201:20-42: Object of class `DecoratorBinder` has no attribute `asyncio` [missing-attribute]
- ERROR asynq/tests/test_tools.py:189:17-77: Object of class `DecoratorBase` has no attribute `__acached_per_instance_cache__` [missing-attribute]
+ ERROR asynq/tests/test_tools.py:189:17-77: Object of class `DecoratorBinder` has no attribute `__acached_per_instance_cache__` [missing-attribute]
- ERROR asynq/tools.py:336:13-33: Object of class `DecoratorBase` has no attribute `dirty` [missing-attribute]
+ ERROR asynq/tools.py:336:13-33: Object of class `DecoratorBinder` has no attribute `dirty` [missing-attribute]
- ERROR asynq/tools.py:338:13-33: Object of class `DecoratorBase` has no attribute `dirty` [missing-attribute]
+ ERROR asynq/tools.py:338:13-33: Object of class `DecoratorBinder` has no attribute `dirty` [missing-attribute]

zulip (https://github.com/zulip/zulip)
- ERROR zerver/tests/test_openapi.py:460:25-54: Object of class `LocaleRegexDescriptor` has no attribute `pattern`
- Object of class `LocaleRegexRouteDescriptor` has no attribute `pattern` [missing-attribute]

pandas (https://github.com/pandas-dev/pandas)
- ERROR pandas/tests/dtypes/test_inference.py:1678:24-43: Object of class `_LengthDescriptor` has no attribute `seconds` [missing-attribute]
+ ERROR pandas/tests/dtypes/test_inference.py:1678:24-43: Object of class `int` has no attribute `seconds` [missing-attribute]

rich (https://github.com/Textualize/rich)
- ERROR rich/_win32_console.py:384:39-46: Argument `_CField[c_short, int, c_short | int]` is not assignable to parameter `row` with type `int` in function `WindowsCoordinates.__new__` [bad-argument-type]
- ERROR rich/_win32_console.py:384:52-59: Argument `_CField[c_short, int, c_short | int]` is not assignable to parameter `col` with type `int` in function `WindowsCoordinates.__new__` [bad-argument-type]
- ERROR rich/_win32_console.py:394:39-52: Argument `_CField[c_short, int, c_short | int]` is not assignable to parameter `row` with type `int` in function `WindowsCoordinates.__new__` [bad-argument-type]
- ERROR rich/_win32_console.py:394:58-71: Argument `_CField[c_short, int, c_short | int]` is not assignable to parameter `col` with type `int` in function `WindowsCoordinates.__new__` [bad-argument-type]

optuna (https://github.com/optuna/optuna)
- ERROR optuna/_gp/batched_lbfgsb.py:54:26-61: Object of class `_ParentDescriptor` has no attribute `switch` [missing-attribute]
+ ERROR optuna/_gp/batched_lbfgsb.py:54:26-61: Object of class `NoneType` has no attribute `switch` [missing-attribute]
- ERROR optuna/_gp/batched_lbfgsb.py:73:9-44: Object of class `_ParentDescriptor` has no attribute `switch` [missing-attribute]
+ ERROR optuna/_gp/batched_lbfgsb.py:73:9-44: Object of class `NoneType` has no attribute `switch` [missing-attribute]

@github-actions
Copy link
Copy Markdown

Primer Diff Classification

✅ 3 improvement(s) | ➖ 3 neutral | 6 project(s) total | +12, -25 errors

3 improvement(s) across pip, django-stubs, rich.

Project Verdict Changes Error Kinds Root Cause
pip ✅ Improvement -4 bad-argument-type pyrefly/lib/alt/class/class_field.rs
django-stubs ✅ Improvement +2, -7 New bad-override errors (FileField.__get__, Tuple.output_field) pyrefly/lib/alt/class/class_field.rs
asynq ➖ Neutral +7, -7 missing-attribute
pandas ➖ Neutral +1, -1 missing-attribute
rich ✅ Improvement -4 bad-argument-type pyrefly/lib/alt/class/class_field.rs
optuna ➖ Neutral +2, -2 missing-attribute
Detailed analysis

✅ Improvement (3)

pip (-4)

All 4 removed errors are false positives being fixed. The _COORD structure from ctypes.wintypes has fields X and Y declared in typeshed's .pyi stubs as _CField descriptors. _CField implements __get__ which returns int for c_short fields. Before this PR, pyrefly didn't apply descriptor semantics to stub-declared fields, so it saw the raw _CField type instead of the __get__ return type int. The PR correctly extends descriptor detection to stub declarations (ClassFieldInitialization::Magic), per the descriptor protocol spec at https://typing.readthedocs.io/en/latest/spec/datamodel.html#descriptors. This is a clear improvement — the code is correct and pyrefly was wrong to flag it.
Attribution: The change in pyrefly/lib/alt/class/class_field.rs in the AnswersSolver method modified the condition for when descriptor semantics apply. Previously, descriptors were only recognized for ClassFieldInitialization::ClassBody(_), ClassVar-annotated fields, or special descriptor types. The PR added ClassFieldInitialization::Magic to the match pattern, which covers fields declared in .pyi stub files. This directly fixes the _CField descriptor resolution for ctypes structures declared in typeshed stubs, causing coord.Y and coord.X to correctly resolve to int instead of _CField[c_short, int, c_short | int].

django-stubs (+2, -7)

New bad-override errors (FileField.get, Tuple.output_field): These are false positives on django-stubs, a well-tested type stubs project. Neither mypy nor pyright flags these. The FileField.__get__ override is explicitly marked @override and intentionally returns more specific types. The Tuple.output_field is a standard attribute narrowing. Both are side effects of the broader Magic initialization handling.
Removed bad-override errors on .field attributes: These were false positives where pyrefly incorrectly flagged standard django-stubs patterns of narrowing .field type annotations in subclasses (e.g., FileDescriptor.field: FileField narrowing DeferredAttribute.field). Neither mypy nor pyright flagged these. Removing them is correct.

Overall: Net effect: 7 false positives removed, 2 false positives added. The removals are clearly correct — standard django-stubs patterns that mypy/pyright accept. The additions are false positives on a well-tested stubs project (pyrefly-only, 0/2 co-reported). Overall this is a net improvement (5 fewer false positives), though the 2 new errors are minor regressions.

Per-category reasoning:

  • New bad-override errors (FileField.get, Tuple.output_field): These are false positives on django-stubs, a well-tested type stubs project. Neither mypy nor pyright flags these. The FileField.__get__ override is explicitly marked @override and intentionally returns more specific types. The Tuple.output_field is a standard attribute narrowing. Both are side effects of the broader Magic initialization handling.
  • Removed bad-override errors on .field attributes: These were false positives where pyrefly incorrectly flagged standard django-stubs patterns of narrowing .field type annotations in subclasses (e.g., FileDescriptor.field: FileField narrowing DeferredAttribute.field). Neither mypy nor pyright flagged these. Removing them is correct.

Attribution: The change in pyrefly/lib/alt/class/class_field.rs that added ClassFieldInitialization::Magic to the descriptor-semantics match arm caused both the improvements (removing 7 false positive bad-override errors on .field attributes) and the regressions (introducing 2 new false positive bad-override errors where the broader descriptor handling now misinterprets __get__ overrides and output_field declarations in stubs).

rich (-4)

The PR fixes descriptor semantics for stub-declared fields. In typeshed, ctypes _COORD (aliased as COORD in this code) has fields X and Y of type _CField[c_short, int, c_short | int], which is a descriptor with __get__ returning int. Before the fix, pyrefly didn't apply descriptor protocol to these stub-declared fields, so coord.Y was typed as _CField[c_short, int, c_short | int] instead of int. This caused false bad-argument-type errors when passing coord.Y and coord.X to WindowsCoordinates(row=..., col=...) which expects int. The fix correctly recognizes that ClassFieldInitialization::Magic (used for stub declarations) should trigger descriptor resolution, matching the descriptor protocol spec at https://typing.readthedocs.io/en/latest/spec/datamodel.html#descriptors.
Attribution: The change in pyrefly/lib/alt/class/class_field.rs expanded descriptor semantics to apply when initialization matches ClassFieldInitialization::Magic (in addition to ClassFieldInitialization::ClassBody). The Magic variant covers stub/interface declarations where the runtime initializer is omitted — exactly the case for ctypes _CField fields declared in .pyi stubs. Previously, only ClassBody initialization and ClassVar-annotated fields triggered descriptor resolution. Now stub-declared descriptors like _CField (which has __get__ and __set__) are properly resolved through the descriptor protocol, yielding int instead of the raw _CField type.

➖ Neutral (3)

asynq (+7, -7)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

pandas (+1, -1)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.

optuna (+2, -2)

Same errors at same locations with same error kinds — message wording changed, no behavioral impact.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (3 heuristic, 3 LLM)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect type of descriptor based properties in openpyxl

1 participant