Skip to content

馃悰 fix(resolver): bind only what a guarded statement defines - #760

Merged
gaborbernat merged 1 commit into
tox-dev:mainfrom
gaborbernat:fix-751-guarded-name-extraction
Aug 24, 2026
Merged

馃悰 fix(resolver): bind only what a guarded statement defines#760
gaborbernat merged 1 commit into
tox-dev:mainfrom
gaborbernat:fix-751-guarded-name-extraction

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Aug 24, 2026

Copy link
Copy Markdown
Member

A TYPE_CHECKING statement that fails to execute has its names stood in as forward references, so annotations using them still resolve. Collecting those names walked the statement's whole subtree for Store-context names, which also picks up comprehension, loop and with ... as targets. The module holds no such names at runtime, so binding them created them: a guarded REGISTRY = {type: Dataset[type] for type in (int, str)} put a placeholder under type in the module globals, after which def describe(value: type) resolved to the placeholder in place of the builtin. The parameter rendered as unlinked text, and calling the function in the same process raised TypeError: TypeAliasForwardRef.__call__() takes 1 positional argument but 2 were given. Only the statement's own targets are read now, recursing into the bodies of the statements it wraps. 馃Ч

The Failed guarded type import warning asked whether the failing statement was an ast.Import or ast.ImportFrom, which misses the shapes guarded imports tend to take:

if TYPE_CHECKING:
    if sys.version_info >= (3, 11):
        from typing import Self
    else:
        from typing_extensions import Self

    try:
        from orjson import Fragment
    except ImportError:
        from json_fallback import Fragment

There the import is a leaf of an ast.If or an ast.Try, so a dependency missing from the docs environment went unreported, and the name binding could not make up for it either, because ast.alias is not an ast.Name. The check now looks for an import anywhere in the statement, restoring the diagnostic #741 asked for. Statements that stay silent leave a _LOGGER.debug line, so a typo in a guard stays traceable with -vv.

tox-dev#757 recovered the names of a failed guarded statement with ast.walk over
the whole subtree, which also picks up comprehension, loop and with-as
targets. Those names do not exist at runtime, so setdefault created them:
a guarded `REGISTRY = {type: Dataset[type] for type in (int, str)}` bound
`type` in the module globals, and every later annotation reading `type`
got a placeholder instead of the builtin. Reading the statement's own
targets, and recursing into the bodies of the statements it wraps, keeps
that namespace clean.

Gating the warning on the statement being an import also lost the
diagnostic for a version-gated or try/except import, where the import sits
inside an ast.If or ast.Try. The gate now looks for an import anywhere in
the statement, so those report the absent dependency again, and the
statements that stay silent leave a debug line behind.
@gaborbernat
gaborbernat merged commit a708030 into tox-dev:main Aug 24, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant