Repository navigation
fix(python): resolve annotations through TYPE_CHECKING imports (#1277) - #2700
Merged
Merged
Conversation
The second half of #1277: the reporter's typed-field chain with both annotation imports under `if TYPE_CHECKING:` (the usual shape when the import would cycle), through `typing.TYPE_CHECKING` too. The call resolves by the LSP as with runtime imports, and a runtime call through a TYPE_CHECKING-only name gets no CALLS edge at all. The extraction test reads the new CBMImport.type_checking_only flag, declared here and not yet set. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The import an annotated field's type comes from usually sits under a module-level `if TYPE_CHECKING:` (to avoid an import cycle). parse_python_imports walked only the module's direct statements, so that import was never recorded and the reporter's chain trainer.strategies.process_batch() lost its type exactly where real code writes it. - Extraction records the imports of a module-level `if TYPE_CHECKING:` / `if <module>.TYPE_CHECKING:` consequence block, flagged CBMImport.type_checking_only. elif/else branches stay unrecorded, as every other compound statement's imports do. - One rule for every CALLS import-map builder: cbm_import_binds_runtime_name() (false for a flagged import). The sequential build_import_map applies it in its fast and slow paths; the shared cbm_pxc_build_import_map (= _ex(..., false)) applies it in both loops, so no CALLS import map ever binds an annotation-only name. - The annotation consumers take cbm_pxc_build_import_map_ex(..., true): the cross-file base/field-type resolution and the Python LSP dispatch. The sequential dispatch always takes it; the parallel one builds that map only for a file that records a flagged import and otherwise passes its CALLS map, which is identical there by construction. - The Python LSP binds those names into an annotation-only frame consulted by annotation resolution after the ordinary scope chain: a runtime call through such a name never resolves. Tests: python_type_checking_imports_flagged_issue1277 (extraction), python_type_checking_typed_field_issue1277(_parallel) (pipeline, both venues; controls: no import_map and no LSP CALLS edge through the name). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
… import (#1277) The airflow compat shape imports a name under `if TYPE_CHECKING:` and again in the else branch, usually inside try/except ImportError: the name is bound at runtime, so its TYPE_CHECKING import is not annotation-only and must stay unrecorded, as the runtime import itself does. A star import in an elif or else branch may bind any name. A pure annotation import stays flagged. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…ecorded (#1277) The compat shape imports a name under `if TYPE_CHECKING:` and again in the else branch, usually inside try/except ImportError. The name is bound at runtime there, so its TYPE_CHECKING import is no annotation-only import: recording it flagged made every CALLS import map skip the name and lost the calls and bases that bind through the runtime import (airflow mask_secret, OpenLineageRedactor(SecretsMasker)). The imports of the elif/else branches are collected (any depth, iteratively), every TYPE_CHECKING import whose local name they bind is dropped with them, and a star import there drops the whole block: those names keep the behaviour they had before #1277. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The memory-core linter counts raw allocator calls per file and fails when a file grows. The TYPE_CHECKING change freed the copied local name of an annotation-only import with a new raw free in two import-map builders. build_import_map now asks the predicate on the name's span in the edge properties before copying it (cbm_imports_local_span_binds_runtime_name, which cbm_imports_local_binds_runtime_name now wraps), and the cross-LSP builder routes an annotation-only name through its existing no-value path. Same behaviour, no new raw allocator site. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Owner
Author
|
Lint fix (3ead775): the memory-core linter failed because this change added a raw The graph cannot change from this commit, so the corpus measurement in the description still holds. |
DeusData
added a commit
that referenced
this pull request
Oct 19, 2026
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The import that an annotation's type comes from usually sits under a module-level
if TYPE_CHECKING:, toavoid an import cycle:
parse_python_importswalked only the module's direct statements, so that import was never recorded. Everycall through such an annotation fell to a name guess or got no edge at all.
if TYPE_CHECKING:/if <module>.TYPE_CHECKING:consequence block, flagged
CBMImport.type_checking_only.imports do. A name such a branch binds again is bound at runtime, so its TYPE_CHECKING import is not an
annotation-only import, and it stays unrecorded too. This is the compat shape, e.g.
else: try: from airflow.sdk.log import mask_secret. A star import there covers every name of theblock. Those names keep exactly the behaviour they have today.
cbm_import_binds_runtime_name(), which is false for aflagged import. So no CALLS import map binds an annotation-only name, and a runtime call through one gets
no edge.
Python LSP. The LSP binds them in an annotation-only frame, consulted by annotation resolution after the
ordinary scope chain.
No index-format change.
Evidence
Base main 1346ac4 vs this change, 26 Python repos (manifest v1), production builds, macOS arm64,
--repeat 2, every repeat byte-identical.lsp_method16,301,lsp_method_union118,lsp_operator_dunder111 andlsp_callable_alias12, plus a few namestrategies (86). The biggest share is home-assistant (14,430), whose integrations import
HomeAssistant,ConfigEntryand friends under TYPE_CHECKING. Then scrapy 634, airflow 379, sqlalchemy 219, poetry 181,sentry 154 and mypy 136. TESTS follow from the new CALLS.
symbol, as for runtime imports.
name guesses that are now gone:
event.get_event_type()anddag_maker.create_dagrun()had been boundto same-named module functions, and an openlineage
Dataset(...)to an unrelated task-sdkDataset.cross-component name guesses.
SentryPrinter(honcho.printer.Printer)has an external base, and the in-repoPrinterit was bound to was wrong.Judged precision of the additions
Seeded samples (seed 20261017), each judged against its call line and target definition: 50/50 correct.
hass.config_entries.<method>(...)reachingConfigEntries.<method>, becauseHomeAssistant.config_entriesis annotated with a type thatcore.pyimports under TYPE_CHECKING. The rest go through
hass.auth(AuthManager),hass.http(
HomeAssistantHTTP), the recorder (Recorder) and annotated entity parameters. A spot check of thatremainder (12 edges) is also correct.
ansible
self._loaderreachesDataLoader, scrapyself.statsreachesStatsCollector, mypybuilderreaches
IRBuilder/LowLevelIRBuilder, airflowdag_makerreachesDagMaker, sympyExprreachesBasicmethods, and so on.ConfigEntriesmethod it calls.Tests
python_type_checking_imports_flagged_issue1277(extraction): TYPE_CHECKING andtyping.TYPE_CHECKINGimports are recorded and flagged. A runtime import is not flagged, and an else branch and an ordinary
ifstay unrecorded.python_type_checking_rebound_in_else_unrecorded_issue1277(extraction): a name the else branch bindsagain under try/except is not recorded. A star import in an elif covers every name, and a pure
annotation import stays flagged.
python_type_checking_typed_field_issue1277{,_parallel}(pipeline): the reporter's typed-field chainwith both imports under TYPE_CHECKING resolves through the LSP. Controls: there is no import_map and no
LSP CALLS edge through the annotation-only name.
cross_repo, py_lsp, py_lsp_stress, py_lsp_scale, lsp_resolution_probe, edge_imports, edge_types_probe,
convergence_probe, matrix_known_classes, parallel, incremental, integration) pass.
Part of #1277 (the TYPE_CHECKING half).