Skip to content
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

[flake8-builtins] Make strict module name comparison optional (A005) #15951

Merged
merged 15 commits into from
Feb 10, 2025

Conversation

ntBre
Copy link
Contributor

@ntBre ntBre commented Feb 4, 2025

Summary

This PR adds the configuration option lint.flake8-builtins.builtins-strict-checking, which is used in A005 to determine whether the fully-qualified module name (relative to the project root or source directories) should be checked instead of just the final component as is currently the case.

As discussed in #15399 (comment), the default value of the new option is false on preview, so modules like utils.logging from the initial report are no longer flagged by default. For non-preview the default is still strict checking.

Test Plan

New A005 test module with the structure reported in #15399.

Fixes #15399

Copy link
Contributor

github-actions bot commented Feb 4, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

ℹ️ ecosystem check detected linter changes. (+0 -105 violations, +0 -0 fixes in 6 projects; 49 projects unchanged)

Snowflake-Labs/snowcli (+0 -7 violations, +0 -0 fixes)

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

- src/snowflake/cli/_plugins/cortex/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- src/snowflake/cli/_plugins/notebook/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- src/snowflake/cli/api/console/abc.py:1:1: A005 Module `abc` shadows a Python standard-library module
- src/snowflake/cli/api/console/enum.py:1:1: A005 Module `enum` shadows a Python standard-library module
- src/snowflake/cli/api/errno.py:1:1: A005 Module `errno` shadows a Python standard-library module
- src/snowflake/cli/api/output/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- src/snowflake/cli/api/utils/types.py:1:1: A005 Module `types` shadows a Python standard-library module

apache/airflow (+0 -49 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

- airflow/api_connexion/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- airflow/api_fastapi/common/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- airflow/api_fastapi/execution_api/datamodels/token.py:1:1: A005 Module `token` shadows a Python standard-library module
- airflow/api_fastapi/logging/__init__.py:1:1: A005 Module `logging` shadows a Python standard-library module
- airflow/io/__init__.py:1:1: A005 Module `io` shadows a Python standard-library module
- airflow/io/utils/stat.py:1:1: A005 Module `stat` shadows a Python standard-library module
- airflow/models/operator.py:1:1: A005 Module `operator` shadows a Python standard-library module
- airflow/operators/email.py:1:1: A005 Module `email` shadows a Python standard-library module
- airflow/secrets/__init__.py:1:1: A005 Module `secrets` shadows a Python standard-library module
- airflow/serialization/serializers/datetime.py:1:1: A005 Module `datetime` shadows a Python standard-library module
- airflow/utils/email.py:1:1: A005 Module `email` shadows a Python standard-library module
- airflow/utils/json.py:1:1: A005 Module `json` shadows a Python standard-library module
- airflow/utils/platform.py:1:1: A005 Module `platform` shadows a Python standard-library module
- airflow/utils/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- airflow/utils/warnings.py:1:1: A005 Module `warnings` shadows a Python standard-library module
- providers/cncf/kubernetes/src/airflow/providers/cncf/kubernetes/operators/resource.py:1:1: A005 Module `resource` shadows a Python standard-library module
- providers/common/io/src/airflow/providers/common/io/__init__.py:1:1: A005 Module `io` shadows a Python standard-library module
- providers/common/io/tests/provider_tests/common/io/__init__.py:1:1: A005 Module `io` shadows a Python standard-library module
- providers/common/io/tests/system/common/io/__init__.py:1:1: A005 Module `io` shadows a Python standard-library module
- providers/fab/src/airflow/providers/fab/www/api_connexion/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- providers/google/src/airflow/providers/google/cloud/secrets/__init__.py:1:1: A005 Module `secrets` shadows a Python standard-library module
- providers/google/src/airflow/providers/google/suite/hooks/calendar.py:1:1: A005 Module `calendar` shadows a Python standard-library module
- providers/google/tests/provider_tests/google/cloud/secrets/__init__.py:1:1: A005 Module `secrets` shadows a Python standard-library module
... 26 additional changes omitted for project

apache/superset (+0 -15 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

- superset/advanced_data_type/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- superset/commands/dashboard/copy.py:1:1: A005 Module `copy` shadows a Python standard-library module
- superset/dashboards/permalink/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- superset/databases/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- superset/distributed_lock/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- superset/explore/permalink/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- superset/key_value/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- superset/reports/notifications/email.py:1:1: A005 Module `email` shadows a Python standard-library module
- superset/reports/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- superset/sqllab/permalink/types.py:1:1: A005 Module `types` shadows a Python standard-library module
... 5 additional changes omitted for project

bokeh/bokeh (+0 -17 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

- examples/interaction/widgets/fileinput.py:1:1: A005 Module `fileinput` shadows a Python standard-library module
- src/bokeh/application/handlers/code.py:1:1: A005 Module `code` shadows a Python standard-library module
- src/bokeh/command/subcommands/json.py:1:1: A005 Module `json` shadows a Python standard-library module
- src/bokeh/core/property/datetime.py:1:1: A005 Module `datetime` shadows a Python standard-library module
- src/bokeh/core/property/enum.py:1:1: A005 Module `enum` shadows a Python standard-library module
- src/bokeh/core/property/json.py:1:1: A005 Module `json` shadows a Python standard-library module
- src/bokeh/core/property/string.py:1:1: A005 Module `string` shadows a Python standard-library module
- src/bokeh/core/property/struct.py:1:1: A005 Module `struct` shadows a Python standard-library module
- src/bokeh/core/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- src/bokeh/core/validation/warnings.py:1:1: A005 Module `warnings` shadows a Python standard-library module
... 7 additional changes omitted for project

latchbio/latch (+0 -10 violations, +0 -0 fixes)

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

- src/latch/functions/secrets.py:1:1: A005 Module `secrets` shadows a Python standard-library module
- src/latch/idl/core/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- src/latch/registry/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- src/latch/registry/upstream_types/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- src/latch/types/__init__.py:1:1: A005 Module `types` shadows a Python standard-library module
- src/latch/types/glob.py:1:1: A005 Module `glob` shadows a Python standard-library module
- src/latch/types/json.py:1:1: A005 Module `json` shadows a Python standard-library module
- src/latch_cli/exceptions/traceback.py:1:1: A005 Module `traceback` shadows a Python standard-library module
- src/latch_cli/services/cp/glob.py:1:1: A005 Module `glob` shadows a Python standard-library module
- src/latch_cli/snakemake/config/parser.py:1:1: A005 Module `parser` shadows a Python standard-library module

zulip/zulip (+0 -7 violations, +0 -0 fixes)

ruff check --no-cache --exit-zero --ignore RUF9 --no-fix --output-format concise --preview --select ALL

- zerver/actions/typing.py:1:1: A005 Module `typing` shadows a Python standard-library module
- zerver/lib/profile.py:1:1: A005 Module `profile` shadows a Python standard-library module
- zerver/lib/queue.py:1:1: A005 Module `queue` shadows a Python standard-library module
- zerver/lib/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- zerver/lib/url_preview/types.py:1:1: A005 Module `types` shadows a Python standard-library module
- zerver/views/typing.py:1:1: A005 Module `typing` shadows a Python standard-library module
- zerver/webhooks/json/__init__.py:1:1: A005 Module `json` shadows a Python standard-library module

Changes by rule (1 rules affected)

code total + violation - violation + fix - fix
A005 105 0 105 0 0

@ntBre ntBre added rule Implementing or modifying a lint rule configuration Related to settings and configuration labels Feb 4, 2025
@MichaReiser
Copy link
Member

Have you considered implementing the approach mentioned in #15399 (comment) and if so, why did you decide against it?

@ntBre
Copy link
Contributor Author

ntBre commented Feb 5, 2025

I could certainly be missing something here, but from my reading of the comment, this implementation (with the bugfix I just added) should cover that case. Possibly this should be added as a test, but I just added this directrory structure locally:

.
├── abc
│   └── __init__.py
├── collections
│   ├── __init__.py
│   ├── abc
│   │   └── __init__.py
│   └── foobar
│       └── __init__.py
├── foobar
│   ├── abc
│   │   └── __init__.py
│   └── collections
│       ├── __init__.py
│       ├── abc
│       │   └── __init__.py
│       └── foobar
│           └── __init__.py
├── ruff.toml
└── urlparse
    └── __init__.py

And ran my debug build of ruff:

/home/brent/astral/ruff/target/debug/ruff check --no-cache --select A005  .
abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
collections/abc/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
collections/foobar/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
Found 4 errors.

If I enable strict checking, I get 4 additional errors for the foobar subpackages, which is the behavior I understood from the comment.

Again, I could be missing something, but we don't really have to treat the fully-qualified path or worry about parent-child relationships mentioned in the comment directly because we check each of the files separately.

crates/ruff_linter/src/checkers/filesystem.rs Outdated Show resolved Hide resolved
Comment on lines 1147 to 1152
default = r#"false"#,
value_type = "bool",
example = "builtins-strict-checking = bool"
)]
/// Compare module names instead of full module paths.
pub builtins_strict_checking: Option<bool>,
Copy link
Member

Choose a reason for hiding this comment

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

I think we have to keep the existing default and wait for the next minor to change it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense, I'll set the default back to true. Is there a special label I need to apply here or otherwise track that for the next minor release?

Copy link
Member

Choose a reason for hiding this comment

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

We can change the default for preview. Although I'm not sure if we did that before but it would give us some preview testing.

The easiest for tracking is to create an issue and tag it with the next milestone. Or you can create a follow up PR, mark it as breaking, and add it to the milestone.

Comment on lines 77 to 80
let (module_name, parent) = if is_module_file(path) {
(
package.path().file_name().unwrap().to_string_lossy(),
package.path().parent(),
)
} else {
path.file_stem().unwrap().to_string_lossy()
(path.file_stem().unwrap().to_string_lossy(), path.parent())
};
Copy link
Member

Choose a reason for hiding this comment

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

Nit: the only difference between the two branches seems to be whether we call the operations on package.path or path. Could we change the if to either return package.path or path and then extract the module_name and parent outside the if?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's also file_name vs file_stem to remove the .py from the non-module file, but this did look a bit better before I made it a tuple. I'll think about an improvement.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I missed that

@ntBre
Copy link
Contributor Author

ntBre commented Feb 5, 2025

I think my example from earlier was actually misleading because I didn't have a foobar/__init__.py file. With that file, the existing behavior in ruff is already as described in the issue comment. We get the four errors above and don't error on the foobar/ subtree. This is because the package.path() always seems to return the root package, rather than the leaf package. For example, when path is foobar/abc/__init__.py, the package.path() is still foobar.

Related to your is_module_path suggestion, I think if we want the strictness option to have any effect, we should actually check path.file_name() in both cases and not even use the package argument at all.

I noticed this when testing a default strict value and not being able to reproduce the eight-error case.

@ntBre
Copy link
Contributor Author

ntBre commented Feb 5, 2025

Sorry @MichaReiser, I now believe that you were right all along. I ended up writing a few integration tests with the exact module structure above (including foobar/__init__.py), and trying to pass those pushed me back toward the path-component implementation.

I also changed the default for builtins_strict_checking to true, so I expected the stable ecosystem check to show no changes and the preview check to match the previous report (-88), but apparently I've made the default even more strict than it was before. Is that okay, or should I try to get rid of the new violations? It looks like these are true positives, if our intention is to flag any module with a name from the standard library.

Once the non-strict behavior becomes default, there will still be a substantial net decrease in violations too.

@ntBre ntBre added the preview Related to preview mode features label Feb 5, 2025
@MichaReiser
Copy link
Member

MichaReiser commented Feb 6, 2025

but apparently I've made the default even more strict than it was before. Is that okay, or should I try to get rid of the new violations?

Can you explain in what ways you made the check more strict than it used to be before? I ask because it depends if the changed behavior classifies as a bug fix (fine to do in a patch release) or a behavior change (it depends but generally no)

@ntBre
Copy link
Contributor Author

ntBre commented Feb 6, 2025

Can you explain in what ways you made the check more strict than it used to be before? I ask because it depends if the changed behavior classifies as a bug fix (fine to do in a patch release) or a behavior change (it depends but generally no)

For modules, the original code used package.path() to get the module_name, which appears to give the top-level module name and I think could be considered a bug. This leads to the following output on main:

path                                   module_name
----                                   ------------
foobar/collections/abc/__init__.py     foobar
foobar/abc/__init__.py                 foobar
collections/__init__.py                collections
abc/__init__.py                        abc
urlparse/__init__.py                   urlparse
collections/abc/__init__.py            collections
foobar/collections/__init__.py         foobar
foobar/collections/foobar/__init__.py  foobar
foobar/__init__.py                     foobar
collections/foobar/__init__.py         collections
foobar/logging.py                      logging      # new, not in tree above

The package.path() behavior is different from the treatment of .py files, where main uses the basename of the file. Instead of checking one or the other, we now consider each component of the module path separately in strict mode, so all of the foobar/ subtree (abc, collections, collections/foobar) is marked with errors.

I think we're being more faithful to the ## What it does section in the docs now:

Checks for modules that use the same names as Python standard-library modules.

But I'm definitely wary of introducing more diagnostics, especially when the issue I set out to fix was for too many diagnostics.

@ntBre
Copy link
Contributor Author

ntBre commented Feb 6, 2025

Thanks again for the feedback here and on Discord! I think I've handled all of the review comments here and also revised the implementation to reflect the upstream behavior better. To help visualize the behavior, here's a table of the strict vs non-strict behavior, operating on the tree from above (again with foobar/logging.py added):

Path Strict Non-strict Main Flake8
abc/__init__.py
collections/__init__.py
collections/abc/__init__.py
collections/foobar/__init__.py
foobar/__init__.py
foobar/abc/__init__.py
foobar/collections/__init__.py
foobar/collections/abc/__init__.py
foobar/collections/foobar/__init__.py
urlparse/__init__.py
foobar/logging.py

Like #15399 (comment), ✅ means we emit a diagnostic and ❌ means no diagnostic.

We now match the behavior of the upstream flake8-builtins in strict mode, but allow configuring a non-strict mode too.

@MichaReiser
Copy link
Member

Thanks for the nice table. I think we should split out the bug fix from the preview behavior change for a clear changelog entry. I hope it isn't too tedious (we can update the changelog manually if it is)

@ntBre
Copy link
Contributor Author

ntBre commented Feb 6, 2025

I think we should split out the bug fix from the preview behavior change for a clear changelog entry.

Sure, no problem! Just to make sure, the bug fix is matching the behavior of upstream (so most of the changes in the rule file, minus the new option), and the preview change is everything to do with the new config option?

@MichaReiser
Copy link
Member

Exactly

ntBre added a commit that referenced this pull request Feb 6, 2025
See #15951 for the original discussion and reviews. This is just the first half
of that PR (reaching parity with `flake8-builtins` without adding any new
configuration options) split out for nicer changelog entries.
@MichaReiser MichaReiser changed the base branch from main to brent/a005-bugfix February 7, 2025 08:07
@MichaReiser MichaReiser changed the base branch from brent/a005-bugfix to main February 7, 2025 08:07
@MichaReiser
Copy link
Member

MichaReiser commented Feb 7, 2025

I think I'll have to wait to review this one until your other PR lands. I put it back in draft mode and you can signal that it's ready by marking it ready for review again.

@MichaReiser MichaReiser marked this pull request as draft February 7, 2025 08:08
ntBre added a commit that referenced this pull request Feb 7, 2025
…16006)

See #15951 for the original discussion and reviews. This is just the
first half of that PR (reaching parity with `flake8-builtins` without
adding any new configuration options) split out for nicer changelog
entries.

For posterity, here's a script for generating the module structure that
was useful for interactive testing and creating the table
[here](#15951 (comment)).
The results for this branch are the same as the `Strict` column there,
as expected.

```shell
mkdir abc collections foobar urlparse

for i in */
do
	touch $i/__init__.py
done	

cp -r abc foobar collections/.
cp -r abc collections foobar/.

touch ruff.toml

touch foobar/logging.py
```

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
@ntBre ntBre marked this pull request as ready for review February 8, 2025 18:06
@ntBre
Copy link
Contributor Author

ntBre commented Feb 8, 2025

I think this should be ready to review again. It's now only a couple of lines changed in the rule itself, the addition of the config option, and adding the right option value to a couple of existing tests.

@ntBre ntBre merged commit 88b543d into main Feb 10, 2025
21 checks passed
@ntBre ntBre deleted the brent/a005 branch February 10, 2025 00:33
dcreager added a commit that referenced this pull request Feb 10, 2025
* main: (991 commits)
  [red-knot] Resolve `Options` to `Settings` (#16000)
  Bump version to 0.9.6 (#16074)
  Revert tailwindcss v4 update (#16075)
  Improve migration document (#16072)
  Fix reference definition labels for backtick-quoted shortcut links (#16035)
  RUF009 should behave similar to B008 and ignore attributes with immutable types (#16048)
  [`pylint`] Also report when the object isn't a literal (`PLE1310`) (#15985)
  Update Rust crate rustc-hash to v2.1.1 (#16060)
  Root exclusions in the server to project root (#16043)
  Directly include `Settings` struct for the server (#16042)
  Update Rust crate clap to v4.5.28 (#16059)
  Update Rust crate strum_macros to 0.27.0 (#16065)
  Update NPM Development dependencies (#16067)
  Update Rust crate uuid to v1.13.1 (#16066)
  Update Rust crate strum to 0.27.0 (#16064)
  Update pre-commit dependencies (#16063)
  Update dependency ruff to v0.9.5 (#16062)
  Update Rust crate toml to v0.8.20 (#16061)
  [`flake8-builtins`] Make strict module name comparison optional (`A005`) (#15951)
  [`ruff`] Indented form feeds (`RUF054`) (#16049)
  ...
@DaniBodor
Copy link
Contributor

I believe that the documentation for this was not yet updated, right? Was trying to read there what this new setting is without having to go through pages of discussion about it, but dont see it there.

@MichaReiser
Copy link
Member

The website should be updated (see) but yes, we should reference the option in the rule's documentation. @ntBre would you mind following up on this?

@DaniBodor
Copy link
Contributor

I indeed meant the rule documentation, which is where i (and i guess most people) normally first land when looking up how to change settings regarding a specific rule.

@ntBre
Copy link
Contributor Author

ntBre commented Feb 11, 2025

Will do!

ntBre added a commit that referenced this pull request Feb 11, 2025
Follow-up to #15951 to update
* the options links in A005 to reference `lint.flake8-builtins.builtins-strict-checking`
* the description of the rule to explain strict vs non-strict checking
* the option documentation to point back to the rule
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
configuration Related to settings and configuration preview Related to preview mode features rule Implementing or modifying a lint rule
Projects
None yet
Development

Successfully merging this pull request may close these issues.

A005 stdlib-module-shadowing warns that utils.logging shadows python logging module
3 participants