Repository navigation
Add support for __lazy_modules__
#28459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
0f57f47
dafb8b8
4d57760
0b0d49a
e0bed85
0b4d67d
b586141
e30f6a4
3b97118
d3e4552
dd77ba8
dd6cbf2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| # `lazy-import-mismatch` (`TID254`) | ||
|
|
||
| ## Explicit lazy imports | ||
|
|
||
| ```toml | ||
| target-version = "py315" | ||
|
|
||
|
|
@@ -12,7 +14,7 @@ require-lazy = ["typing", "package.deferred"] | |
| ban-lazy = ["this", "package.eager"] | ||
| ``` | ||
|
|
||
| ## Multiple imports | ||
| ### Multiple imports | ||
|
|
||
| When names in one statement have conflicting policies, we report the mismatch without a fix. | ||
| Changing the entire statement would violate the other name's policy and cause a fix loop. | ||
|
|
@@ -34,7 +36,7 @@ error[TID254]: `typing` should be imported lazily | |
| help: Convert to a lazy import | ||
| ``` | ||
|
|
||
| ## Multiple imported members | ||
| ### Multiple imported members | ||
|
|
||
| The same restriction applies to importing multiple members from one module, whether the statement | ||
| is eager or lazy. | ||
|
|
@@ -55,3 +57,139 @@ error[TID254]: `package.deferred` should be imported lazily | |
| | ^^^^^^^^ | ||
| help: Convert to a lazy import | ||
| ``` | ||
|
|
||
| ## Module declarations | ||
|
|
||
| ```toml | ||
| target-version = "py314" | ||
|
|
||
| [lint] | ||
| preview = true | ||
| select = ["TID254"] | ||
|
|
||
| [lint.flake8-tidy-imports] | ||
| require-lazy = "all" | ||
| ``` | ||
|
|
||
| ### Literal declarations | ||
|
|
||
| [PEP 810](https://peps.python.org/pep-0810/#semantics) allows a module-level `__lazy_modules__` | ||
| collection to identify lazy imports. Ruff recognizes literal lists, tuples, and sets, including | ||
| annotated assignments. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = ["json"] | ||
| import json | ||
|
|
||
| __lazy_modules__ = ("pathlib",) | ||
| import pathlib | ||
|
|
||
| __lazy_modules__: set[str] = {"collections"} | ||
| import collections | ||
|
|
||
| import math # error: [lazy-import-mismatch] | ||
| ``` | ||
|
|
||
| ### Exact module matching | ||
|
|
||
| Entries match module names independently of local aliases. A `from` import checks its containing | ||
| module, and listing a package does not include its submodules. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = ["json", "xml"] | ||
| import json as renamed | ||
| from json import dumps, loads | ||
| import xml.dom # error: [lazy-import-mismatch] | ||
| ``` | ||
|
|
||
| ### Empty declarations on older Python versions | ||
|
|
||
| An empty declaration enables lazy-import policies even when the target version predates the `lazy` | ||
| keyword. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = [] | ||
| import json # error: [lazy-import-mismatch] | ||
| ``` | ||
|
Comment on lines
+105
to
+113
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's possible that we don't want to do this, but it made sense to me to consider a Similarly, I added the "In the future..." note below in analogy to #18502. |
||
|
|
||
| ### Older Python versions without a declaration | ||
|
|
||
| Without a declaration, the policy is skipped on target versions that do not support `lazy` syntax. | ||
|
|
||
| In the future, the rule could create a `__lazy_modules__` declaration to offer fixes on older targets | ||
| without requiring an existing declaration. | ||
|
|
||
| ```py | ||
| import json | ||
| ``` | ||
|
|
||
| ### Reassigned declarations | ||
|
|
||
| Each import uses the declaration present when the checker reaches it. An annotation without a value | ||
| does not replace the declaration. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = ["json"] | ||
| import json | ||
| __lazy_modules__: list[str] | ||
| import json as still_lazy | ||
|
|
||
| __lazy_modules__ = [] | ||
| import json as eager # error: [lazy-import-mismatch] | ||
| ``` | ||
|
|
||
| ### Unknown declarations | ||
|
|
||
| An unsupported declaration value does not establish whether the import violates the policy, so no | ||
| diagnostic is emitted. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = configured_modules() | ||
| import json | ||
| ``` | ||
|
Comment on lines
+141
to
+149
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One of the relevant sections from the PEP is here:
We treat this as an unknown laziness, different from certain laziness or certain eagerness. |
||
|
|
||
| ### Per-alias policies | ||
|
|
||
| Each alias is checked independently. Here `json` must become eager and `pathlib` must become lazy. | ||
|
|
||
| ```toml | ||
| target-version = "py315" | ||
|
|
||
| [lint] | ||
| preview = true | ||
| select = ["TID254"] | ||
|
|
||
| [lint.flake8-tidy-imports] | ||
| require-lazy = ["pathlib"] | ||
| ban-lazy = ["json"] | ||
| ``` | ||
|
|
||
| ```py | ||
| __lazy_modules__ = ["json"] | ||
| # error: [lazy-import-mismatch] | ||
| # error: [lazy-import-mismatch] | ||
|
ntBre marked this conversation as resolved.
Outdated
|
||
| import json, pathlib | ||
| ``` | ||
|
|
||
| ### From-import member policies | ||
|
|
||
| A declaration makes every member imported from its module lazy, but policies still apply to | ||
| individual members. | ||
|
|
||
| ```toml | ||
| target-version = "py314" | ||
|
|
||
| [lint] | ||
| preview = true | ||
| select = ["TID254"] | ||
|
|
||
| [lint.flake8-tidy-imports] | ||
| require-lazy = ["pkg.First"] | ||
| ban-lazy = ["pkg.Second"] | ||
| ``` | ||
|
|
||
| ```py | ||
| __lazy_modules__ = ["pkg"] | ||
| from pkg import First | ||
| from pkg import Second # error: [lazy-import-mismatch] | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| # `manual-from-import` (`PLR0402`) | ||
|
|
||
| ```toml | ||
| target-version = "py315" | ||
|
|
||
| [lint] | ||
| select = ["PLR0402"] | ||
| ``` | ||
|
|
||
| ## Both modules listed | ||
|
|
||
| When `__lazy_modules__` contains both the package and its submodule, the fix preserves laziness. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = ["foo", "foo.bar"] | ||
| import foo.bar as bar # snapshot: manual-from-import | ||
| ``` | ||
|
|
||
| ```snapshot | ||
| error[PLR0402]: Use `from foo import bar` in lieu of alias | ||
| --> src/mdtest_snippet.py:2:8 | ||
| | | ||
| 2 | import foo.bar as bar # snapshot: manual-from-import | ||
| | ^^^^^^^^^^^^^^ | ||
| help: Replace with `from foo import bar` | ||
| | | ||
| 1 | __lazy_modules__ = ["foo", "foo.bar"] | ||
| - import foo.bar as bar # snapshot: manual-from-import | ||
| 2 + from foo import bar # snapshot: manual-from-import | ||
| | | ||
| ``` | ||
|
|
||
| ## Only the submodule listed | ||
|
|
||
| A `from` import checks its containing module for membership in `__lazy_modules__`. Rewriting this | ||
| import would make it eager, so the diagnostic has no fix. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = ["foo.bar"] | ||
| import foo.bar as bar # snapshot: manual-from-import | ||
| ``` | ||
|
|
||
| ```snapshot | ||
| error[PLR0402]: Use `from foo import bar` in lieu of alias | ||
| --> src/mdtest_snippet.py:2:8 | ||
| | | ||
| 2 | import foo.bar as bar # snapshot: manual-from-import | ||
| | ^^^^^^^^^^^^^^ | ||
| help: Replace with `from foo import bar` | ||
| ``` | ||
|
|
||
| ## Only the package listed | ||
|
|
||
| Listing a package does not make imports of its submodules lazy. Rewriting this import would make it | ||
| lazy, so the diagnostic has no fix. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = ["foo"] | ||
| import foo.bar as bar # snapshot: manual-from-import | ||
| ``` | ||
|
|
||
| ```snapshot | ||
| error[PLR0402]: Use `from foo import bar` in lieu of alias | ||
| --> src/mdtest_snippet.py:2:8 | ||
| | | ||
| 2 | import foo.bar as bar # snapshot: manual-from-import | ||
| | ^^^^^^^^^^^^^^ | ||
| help: Replace with `from foo import bar` | ||
| ``` | ||
|
|
||
| ## Explicit lazy imports | ||
|
|
||
| The fix preserves the `lazy` keyword, so the import remains lazy even when the package is absent | ||
| from `__lazy_modules__`. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = ["foo.bar"] | ||
| lazy import foo.bar as bar # snapshot: manual-from-import | ||
| ``` | ||
|
|
||
| ```snapshot | ||
| error[PLR0402]: Use `from foo import bar` in lieu of alias | ||
| --> src/mdtest_snippet.py:2:13 | ||
| | | ||
| 2 | lazy import foo.bar as bar # snapshot: manual-from-import | ||
| | ^^^^^^^^^^^^^^ | ||
| help: Replace with `from foo import bar` | ||
| | | ||
| 1 | __lazy_modules__ = ["foo.bar"] | ||
| - lazy import foo.bar as bar # snapshot: manual-from-import | ||
| 2 + lazy from foo import bar # snapshot: manual-from-import | ||
| | | ||
| ``` | ||
|
|
||
| ## Unknown declarations | ||
|
|
||
| When the declaration's contents are unknown, the rule cannot establish whether the fix preserves | ||
| laziness, so no fix is offered. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = configured_lazy_modules | ||
| import foo.bar as bar # snapshot: manual-from-import | ||
| ``` | ||
|
|
||
| ```snapshot | ||
| error[PLR0402]: Use `from foo import bar` in lieu of alias | ||
| --> src/mdtest_snippet.py:2:8 | ||
| | | ||
| 2 | import foo.bar as bar # snapshot: manual-from-import | ||
| | ^^^^^^^^^^^^^^ | ||
| help: Replace with `from foo import bar` | ||
| ``` | ||
|
|
||
| ## Imports inside functions | ||
|
|
||
| Imports inside functions are eager regardless of `__lazy_modules__`, so an unknown declaration does | ||
| not prevent a fix. | ||
|
|
||
| ```py | ||
| __lazy_modules__ = configured_lazy_modules | ||
|
|
||
|
|
||
| def import_in_function(): | ||
| import foo.bar as bar # snapshot: manual-from-import | ||
| ``` | ||
|
|
||
| ```snapshot | ||
| error[PLR0402]: Use `from foo import bar` in lieu of alias | ||
| --> src/mdtest_snippet.py:5:12 | ||
| | | ||
| 5 | import foo.bar as bar # snapshot: manual-from-import | ||
| | ^^^^^^^^^^^^^^ | ||
| help: Replace with `from foo import bar` | ||
| | | ||
| 4 | def import_in_function(): | ||
| - import foo.bar as bar # snapshot: manual-from-import | ||
| 5 + from foo import bar # snapshot: manual-from-import | ||
| | | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this an idea for another rule that warns about earlier imports that are later marked as lazy (same when using the
lazy importsyntax?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be!