Skip to content

[flake8-tidy-imports] Allow lazy imports to be used in deferred annotations (TID255) - #28767

Merged
ntBre merged 3 commits into
mainfrom
brent/tid255-deferred-annotations
Sep 22, 2026
Merged

ntBre merged 3 commits into
mainfrom
brent/tid255-deferred-annotations

Conversation

@ntBre

@ntBre ntBre commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR fixes an issue I saw in the ecosystem report for the stabilization PR (#28761 (comment)). In short, we were flagging annotations like this:

__lazy_modules__ = ["pathlib"]

from pathlib import Path

class C:
    path: Path

on Python versions before 3.14, even though they are not actually resolved immediately when
__lazy_modules__ makes the imports lazy (3.15 > 3.14, as it turns out).

Because the semantic model checks the target version when setting TYPING_ONLY_ANNOTATION, we
instead check the other two annotation flags: RUNTIME_EVALUATED_ANNOTATION and
RUNTIME_REQUIRED_ANNOTATION. If an annotation is runtime-evaluated and not runtime-required, and
we didn't already bail out at the semantic.execution_context().is_runtime() check earlier in the
same function, then we know that we're in an annotation that would have been typing-only on 3.14+.

Test Plan

New mdtests and the ecosystem check here. I expect -12 scikit-build-core diagnostics compared to main.

…otations (`TID255`)

Summary
--

This PR fixes an issue I saw in the ecosystem report for the stabilization PR
(#28761 (comment)). In short, we were flagging
annotations like this:

```py
__lazy_modules__ = ["pathlib"]

from pathlib import Path

class C:
    path: Path
```

on Python versions before 3.14, even though they are not actually resolved immediately when
`__lazy_modules__` makes the imports lazy (3.15 > 3.14, as it turns out).

Because the semantic model checks the target version when setting `TYPING_ONLY_ANNOTATION`, we
instead check the other two annotation flags: `RUNTIME_EVALUATED_ANNOTATION` and
`RUNTIME_REQUIRED_ANNOTATION`. If an annotation is runtime-evaluated and not runtime-required, and
we didn't already bail out at the `semantic.execution_context().is_runtime()` check earlier in the
same function, then we know that we're in an annotation that _would_ have been typing-only on 3.14+.

Test Plan
--

New mdtests
@ntBre ntBre added the bug An issue describing something that isn't working, or a PR that fixes a bug label Sep 21, 2026
@astral-sh-bot

astral-sh-bot Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

ruff-ecosystem results

Linter (stable)

βœ… ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+0 -12 violations, +0 -0 fixes in 1 projects; 58 projects unchanged)

scikit-build/scikit-build-core (+0 -12 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --no-fix --output-format concise --preview

- src/scikit_build_core/file_api/model/common.py:21:13: lazy-import-immediately-resolved Lazy import `Path` is resolved immediately
- src/scikit_build_core/file_api/model/common.py:22:12: lazy-import-immediately-resolved Lazy import `Path` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:18:30: lazy-import-immediately-resolved Lazy import `Path` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:19:27: lazy-import-immediately-resolved Lazy import `Path` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:20:36: lazy-import-immediately-resolved Lazy import `Path` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:21:25: lazy-import-immediately-resolved Lazy import `Path` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:27:11: lazy-import-immediately-resolved Lazy import `Optional` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:27:20: lazy-import-immediately-resolved Lazy import `Path` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:28:22: lazy-import-immediately-resolved Lazy import `Optional` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:29:9: lazy-import-immediately-resolved Lazy import `Optional` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:30:14: lazy-import-immediately-resolved Lazy import `Optional` is resolved immediately
- src/scikit_build_core/file_api/model/toolchains.py:31:13: lazy-import-immediately-resolved Lazy import `Optional` is resolved immediately

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
lazy-import-immediately-resolved 12 0 12 0 0

@ntBre
ntBre marked this pull request as ready for review September 21, 2026 22:03
@ntBre
ntBre requested a review from MichaReiser September 21, 2026 22:04
annotated: Annotated[Path, Path(".")] # both ok
```

#### Runtime-required annotations

@MichaReiser MichaReiser Sep 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add a regression test for dataclass, to acknowledge that this may require user configuration

__lazy_modules__ = ["pathlib"]
from pathlib import Path
from dataclasses import dataclass

@dataclass
class C:
    path: Path

Here, Path is evaluated immediately. Users could fix this by adding dataclass to runtime required decorators, but Codex points out that this isn't correct.

Dataclasses support quoted annotations whose imports can live under TYPE_CHECKING, so globally marking all dataclass annotations runtime-required could unnecessarily suppress those rules.

I think this is a fine enough limitation for now, but we should document it under known limitations. Unless you can think of a fix (nothing obvious came to my mind)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'll add a test and a note to the docs! I think there are many ways that lazy imports could end up being immediately resolved that Ruff can't follow (e.g. any decorator that uses annotations), but this may be one of the more common cases.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'd mainly document that this rule is known to have false positives in cases where annotations are resolved immediately that Ruff does not understand and that we use the required runtime types settings as an approximation of that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default case causes false negatives rather than false positives in TID255 (we treat the annotation as deferred and not immediately resolved, disabling the rule, despite the decorator resolving it). It also seemed better to me not to document runtime-required-decorators here, as I think using that setting for this rule will give incorrect results in other rules that use it, like the TC rules. That's what I took away from your earlier message at least.

I documented the false negative limitation in the most recent commit.

@ntBre
ntBre merged commit f3dc25d into main Sep 22, 2026
51 checks passed
@ntBre
ntBre deleted the brent/tid255-deferred-annotations branch September 22, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug An issue describing something that isn't working, or a PR that fixes a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants