Compare commits

...

34 Commits

Author SHA1 Message Date
Charlie Marsh
51bda28a7d Bump version to 0.0.192 2022-12-22 17:31:31 -05:00
Reiner Gerecke
cc26051b7a Implement "datetime.UTC alias" check from pyupgrade (#1341) 2022-12-22 17:21:36 -05:00
Charlie Marsh
3ac5a9aa31 Respect --force-exclude for files passed via stdin (#1342) 2022-12-22 16:40:15 -05:00
Charlie Marsh
451047c30d Exclude directly-passed files nested in excluded subdirectories 2022-12-22 15:08:11 -05:00
Charlie Marsh
6907df489b Extend false-positive list for flake8-boolean-trap (#1338) 2022-12-22 10:56:04 -05:00
Charlie Marsh
970f882b03 Set force-exclude for pre-commit in README (#1337) 2022-12-22 10:51:20 -05:00
Charlie Marsh
3eff9a2860 Allow unittest methods in flake8-boolean-trap (#1333) 2022-12-22 08:40:22 -05:00
Charlie Marsh
a4a24a0ef3 Add some more repositories to the user list (#1328) 2022-12-21 22:16:47 -05:00
Charlie Marsh
48e3c046b0 Fix integration tests 2022-12-21 21:25:37 -05:00
Charlie Marsh
03e4f5be8a Bump version to 0.0.191 2022-12-21 21:16:21 -05:00
Charlie Marsh
99657b7d92 Implement E401 ("multiple imports on one line") (#1326) 2022-12-21 21:15:57 -05:00
Charlie Marsh
40377aa1fc Move number of errors to the bottom of the output summary (#1325) 2022-12-21 21:04:26 -05:00
Charlie Marsh
2a37017e8c Add src to Settings hash 2022-12-21 21:01:20 -05:00
Charlie Marsh
ff66d08cef Run generate-options 2022-12-21 20:58:14 -05:00
Charlie Marsh
dad8035eef Support shell expansion in src field (#1324) 2022-12-21 20:57:20 -05:00
Charlie Marsh
bf5fec342c Support shell expansion in extend paths (#1323) 2022-12-21 20:46:38 -05:00
Charlie Marsh
66a6c81ebf Infer package roots when running via stdin (#1321) 2022-12-21 20:30:10 -05:00
Charlie Marsh
5c70f5044b Improve debug logging in flake8-to-ruff (#1320) 2022-12-21 20:05:48 -05:00
Charlie Marsh
953d141ab2 Support code redirects in flake8-to-ruff (#1318) 2022-12-21 19:31:20 -05:00
Charlie Marsh
07dba46039 Extract line length from pyproject.toml Black section (#1317) 2022-12-21 19:05:18 -05:00
Ran Benita
3b02da9d7b Fix false positive DTZ001 on datetime(2000, 1, 1, 0, 0, 0, 0, utc) (#1308) 2022-12-21 19:03:36 -05:00
Charlie Marsh
20234c6156 Bump version to 0.0.190 2022-12-21 16:01:48 -05:00
Charlie Marsh
de767cc026 Avoid used-prior-global-declaration false-positives in f-strings (#1314) 2022-12-21 14:34:09 -05:00
Charlie Marsh
ce1663d302 Allow overriding cache location via RUFF_CACHE_DIR (#1312) 2022-12-21 14:24:10 -05:00
Charlie Marsh
f40e4bcd14 Avoid flagging RUF100 as a RUF100 violation (#1305) 2022-12-20 17:40:10 -05:00
Charlie Marsh
e7d40d435f Avoid F821 false positives for Mypy extensions (#1304) 2022-12-20 16:29:33 -05:00
Charlie Marsh
ef8fe31c0c Bump version to 0.0.189 2022-12-20 13:26:17 -05:00
Charlie Marsh
226f682c99 Avoid DTZ007 false-positives for non-string arguments (#1300) 2022-12-20 13:20:53 -05:00
Hannes Käufler
468ffd29fb [Stylistic/non-functional] Use an r# format string to make json easier to read (#1299) 2022-12-20 12:55:21 -05:00
Charlie Marsh
a61126ab23 Run generate-options 2022-12-19 23:19:05 -05:00
Charlie Marsh
54c7c25861 Revert test changes to setup.py 2022-12-19 20:09:47 -05:00
Charlie Marsh
eff7700d92 Add --force-exclude setting to force exclusions with pre-commit (#1295) 2022-12-19 20:08:59 -05:00
Charlie Marsh
8934f6938d Avoid RET504 errors for intermediary function calls (#1294) 2022-12-19 19:48:09 -05:00
Charlie Marsh
8f0fc3033a Update Arg section checking to match latest pydocstyle (#1293) 2022-12-19 16:39:42 -05:00
66 changed files with 1507 additions and 337 deletions

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.188
rev: v0.0.192
hooks:
- id: ruff

20
Cargo.lock generated
View File

@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.188-dev.0"
version = "0.0.192-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -735,6 +735,8 @@ dependencies = [
"rustc-hash",
"serde",
"serde_json",
"strum",
"strum_macros",
"toml",
]
@@ -1845,7 +1847,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.188"
version = "0.0.192"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1888,6 +1890,7 @@ dependencies = [
"rustpython-parser",
"serde",
"serde_json",
"shellexpand",
"strum",
"strum_macros",
"test-case",
@@ -1901,7 +1904,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.188"
version = "0.0.192"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1919,7 +1922,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.188"
version = "0.0.192"
dependencies = [
"proc-macro2",
"quote",
@@ -2114,6 +2117,15 @@ dependencies = [
"serde",
]
[[package]]
name = "shellexpand"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1c7ddea665294d484c39fd0c0d2b7e35bbfe10035c5fe1854741a57f6880e1"
dependencies = [
"dirs 4.0.0",
]
[[package]]
name = "similar"
version = "2.2.1"

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.188"
version = "0.0.192"
edition = "2021"
rust-version = "1.65.0"
@@ -43,13 +43,14 @@ quick-junit = { version = "0.3.2" }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.188", path = "ruff_macros" }
ruff_macros = { version = "0.0.192", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "c01f014b1269eedcf4bdb45d5fbc62ae2beecf31" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "c01f014b1269eedcf4bdb45d5fbc62ae2beecf31" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "c01f014b1269eedcf4bdb45d5fbc62ae2beecf31" }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
shellexpand = { version = "3.0.0" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }

View File

@@ -39,9 +39,11 @@ Ruff is extremely actively developed and used in major open-source projects like
- [Bokeh](https://github.com/bokeh/bokeh)
- [Zulip](https://github.com/zulip/zulip)
- [Pydantic](https://github.com/pydantic/pydantic)
- [Saleor](https://github.com/saleor/saleor)
- [Hatch](https://github.com/pypa/hatch)
- [Jupyter Server](https://github.com/jupyter-server/jupyter_server)
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Synapse](https://github.com/matrix-org/synapse)
- [Ibis](https://github.com/ibis-project/ibis)
- [Saleor](https://github.com/saleor/saleor)
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -158,11 +160,13 @@ ruff path/to/code/ --watch
Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.188
hooks:
- id: ruff
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.192'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
args: ["--force-exclude"]
```
## Configuration
@@ -517,6 +521,7 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| E401 | MultipleImportsOnOneLine | Multiple imports on one line | |
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | |
| E501 | LineTooLong | Line too long (89 > 88 characters) | |
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | 🛠 |
@@ -623,6 +628,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| UP014 | ConvertNamedTupleFunctionalToClass | Convert `...` from `NamedTuple` functional to class syntax | 🛠 |
| UP015 | RedundantOpenModes | Unnecessary open mode parameters | 🛠 |
| UP016 | RemoveSixCompat | Unnecessary `six` compatibility usage | 🛠 |
| UP017 | DatetimeTimezoneUTC | Use `datetime.UTC` alias | 🛠 |
### pep8-naming (N)
@@ -1219,7 +1225,7 @@ natively, including:
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (3/10)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (17/33)
- [`yesqa`](https://github.com/asottile/yesqa)
Note that, in some cases, Ruff uses different error code prefixes than would be found in the
@@ -1276,7 +1282,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
Ruff can also replace [`isort`](https://pypi.org/project/isort/),
[`yesqa`](https://github.com/asottile/yesqa), [`eradicate`](https://pypi.org/project/eradicate/),
[`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (3/10), and a subset of the rules
implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33).
implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (17/33).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.
@@ -1632,7 +1638,8 @@ exclude = [".venv"]
#### [`extend`](#extend)
A path to a local `pyproject.toml` file to merge into this configuration.
A path to a local `pyproject.toml` file to merge into this configuration. User home
directory and environment variables will be expanded.
To resolve the current `pyproject.toml` file, Ruff will first resolve this base
configuration file, then merge in any properties defined in the current configuration
@@ -1765,6 +1772,30 @@ fixable = ["E", "F"]
---
#### [`force-exclude`](#force-exclude)
Whether to enforce `exclude` and `extend-exclude` patterns, even for paths that are
passed to Ruff explicitly. Typically, Ruff will lint any paths passed in directly, even
if they would typically be excluded. Setting `force-exclude = true` will cause Ruff to
respect these exclusions unequivocally.
This is useful for [`pre-commit`](https://pre-commit.com/), which explicitly passes all
changed files to the [`ruff-pre-commit`](https://github.com/charliermarsh/ruff-pre-commit)
plugin, regardless of whether they're marked as excluded by Ruff's own settings.
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff]
force-exclude = true
```
---
#### [`format`](#format)
The style in which violation messages should be formatted: `"text"` (default),
@@ -1946,7 +1977,8 @@ when resolving imports, `my_package.foo` is considered a first-party import.
This field supports globs. For example, if you have a series of Python packages in
a `python_modules` directory, `src = ["python_modules/*"]` would expand to incorporate
all of the packages in that directory.
all of the packages in that directory. User home directory and environment variables
will also be expanded.
**Default value**: `["."]`

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.188"
version = "0.0.192"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.188"
version = "0.0.192"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.188-dev.0"
version = "0.0.192-dev.0"
edition = "2021"
[lib]
@@ -16,6 +16,8 @@ ruff = { path = "..", default-features = false }
rustc-hash = { version = "1.1.0" }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
toml = { version = "0.5.9" }
[dev-dependencies]

View File

@@ -0,0 +1,65 @@
[build-system]
requires = [
# The minimum setuptools version is specific to the PEP 517 backend,
# and may be stricter than the version required in `setup.cfg`
"setuptools>=40.6.0,!=60.9.0",
"wheel",
# Must be kept in sync with the `install_requirements` in `setup.cfg`
"cffi>=1.12; platform_python_implementation != 'PyPy'",
"setuptools-rust>=0.11.4",
]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 79
target-version = ["py36"]
[tool.pytest.ini_options]
addopts = "-r s --capture=no --strict-markers --benchmark-disable"
markers = [
"skip_fips: this test is not executed in FIPS mode",
"supported: parametrized test requiring only_if and skip_message",
]
[tool.mypy]
show_error_codes = true
check_untyped_defs = true
no_implicit_reexport = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_unused_configs = true
strict_equality = true
[[tool.mypy.overrides]]
module = [
"pretend"
]
ignore_missing_imports = true
[tool.coverage.run]
branch = true
relative_files = true
source = [
"cryptography",
"tests/",
]
[tool.coverage.paths]
source = [
"src/cryptography",
"*.tox/*/lib*/python*/site-packages/cryptography",
"*.tox\\*\\Lib\\site-packages\\cryptography",
"*.tox/pypy/site-packages/cryptography",
]
tests =[
"tests/",
"*tests\\",
]
[tool.coverage.report]
exclude_lines = [
"@abc.abstractmethod",
"@abc.abstractproperty",
"@typing.overload",
"if typing.TYPE_CHECKING",
]

View File

@@ -0,0 +1,91 @@
[metadata]
name = cryptography
version = attr: cryptography.__version__
description = cryptography is a package which provides cryptographic recipes and primitives to Python developers.
long_description = file: README.rst
long_description_content_type = text/x-rst
license = BSD-3-Clause OR Apache-2.0
url = https://github.com/pyca/cryptography
author = The Python Cryptographic Authority and individual contributors
author_email = cryptography-dev@python.org
project_urls =
Documentation=https://cryptography.io/
Source=https://github.com/pyca/cryptography/
Issues=https://github.com/pyca/cryptography/issues
Changelog=https://cryptography.io/en/latest/changelog/
classifiers =
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
License :: OSI Approved :: BSD License
Natural Language :: English
Operating System :: MacOS :: MacOS X
Operating System :: POSIX
Operating System :: POSIX :: BSD
Operating System :: POSIX :: Linux
Operating System :: Microsoft :: Windows
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
Topic :: Security :: Cryptography
[options]
python_requires = >=3.6
include_package_data = True
zip_safe = False
package_dir =
=src
packages = find:
# `install_requires` must be kept in sync with `pyproject.toml`
install_requires =
cffi >=1.12
[options.packages.find]
where = src
exclude =
_cffi_src
_cffi_src.*
[options.extras_require]
test =
pytest>=6.2.0
pytest-benchmark
pytest-cov
pytest-subtests
pytest-xdist
pretend
iso8601
pytz
hypothesis>=1.11.4,!=3.79.2
docs =
sphinx >= 1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0
sphinx_rtd_theme
docstest =
pyenchant >= 1.6.11
twine >= 1.12.0
sphinxcontrib-spelling >= 4.0.1
sdist =
setuptools_rust >= 0.11.4
pep8test =
black
flake8
flake8-import-order
pep8-naming
# This extra is for OpenSSH private keys that use bcrypt KDF
# Versions: v3.1.3 - ignore_few_rounds, v3.1.5 - abi3
ssh =
bcrypt >= 3.1.5
[flake8]
ignore = E203,E211,W503,W504,N818
exclude = .tox,*.egg,.git,_build,.hypothesis
select = E,W,F,N,I
application-import-names = cryptography,cryptography_vectors,tests

View File

@@ -0,0 +1,32 @@
//! Extract Black configuration settings from a pyproject.toml.
use std::path::Path;
use anyhow::Result;
use ruff::settings::types::PythonVersion;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Black {
#[serde(alias = "line-length", alias = "line_length")]
pub line_length: Option<usize>,
#[serde(alias = "target-version", alias = "target_version")]
pub target_version: Option<Vec<PythonVersion>>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Tools {
black: Option<Black>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Pyproject {
tool: Option<Tools>,
}
pub fn parse_black_options<P: AsRef<Path>>(path: P) -> Result<Option<Black>> {
let contents = std::fs::read_to_string(path)?;
Ok(toml::from_str::<Pyproject>(&contents)?
.tool
.and_then(|tool| tool.black))
}

View File

@@ -11,13 +11,20 @@ use ruff::{
pep8_naming,
};
use crate::black::Black;
use crate::plugin::Plugin;
use crate::{parser, plugin};
pub fn convert(
flake8: &HashMap<String, Option<String>>,
config: &HashMap<String, HashMap<String, Option<String>>>,
black: Option<&Black>,
plugins: Option<Vec<Plugin>>,
) -> Result<Pyproject> {
// Extract the Flake8 section.
let flake8 = config
.get("flake8")
.expect("Unable to find flake8 section in INI file");
// Extract all referenced check code prefixes, to power plugin inference.
let mut referenced_codes: BTreeSet<CheckCodePrefix> = BTreeSet::default();
for (key, value) in flake8 {
@@ -54,10 +61,18 @@ pub fn convert(
plugin::resolve_select(
flake8,
&plugins.unwrap_or_else(|| {
plugin::infer_plugins_from_options(flake8)
.into_iter()
.chain(plugin::infer_plugins_from_codes(&referenced_codes))
.collect()
let from_options = plugin::infer_plugins_from_options(flake8);
if !from_options.is_empty() {
eprintln!("Inferred plugins from settings: {from_options:#?}");
}
let from_codes = plugin::infer_plugins_from_codes(&referenced_codes);
if !from_codes.is_empty() {
eprintln!(
"Inferred plugins from referenced check codes: {:#?}",
from_codes
);
}
from_options.into_iter().chain(from_codes).collect()
}),
)
});
@@ -236,6 +251,19 @@ pub fn convert(
options.pep8_naming = Some(pep8_naming);
}
// Extract any settings from the existing `pyproject.toml`.
if let Some(black) = black {
if let Some(line_length) = &black.line_length {
options.line_length = Some(*line_length);
}
if let Some(target_version) = &black.target_version {
if let Some(target_version) = target_version.iter().min() {
options.target_version = Some(*target_version);
}
}
}
// Create the pyproject.toml.
Ok(Pyproject::new(options))
}
@@ -255,7 +283,11 @@ mod tests {
#[test]
fn it_converts_empty() -> Result<()> {
let actual = convert(&HashMap::from([]), None)?;
let actual = convert(
&HashMap::from([("flake8".to_string(), HashMap::default())]),
None,
None,
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
@@ -268,6 +300,7 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
@@ -302,7 +335,11 @@ mod tests {
#[test]
fn it_converts_dashes() -> Result<()> {
let actual = convert(
&HashMap::from([("max-line-length".to_string(), Some("100".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("max-line-length".to_string(), Some("100".to_string()))]),
)]),
None,
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
@@ -317,6 +354,7 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
@@ -351,7 +389,11 @@ mod tests {
#[test]
fn it_converts_underscores() -> Result<()> {
let actual = convert(
&HashMap::from([("max_line_length".to_string(), Some("100".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("max_line_length".to_string(), Some("100".to_string()))]),
)]),
None,
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
@@ -366,6 +408,7 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
@@ -400,7 +443,11 @@ mod tests {
#[test]
fn it_ignores_parse_errors() -> Result<()> {
let actual = convert(
&HashMap::from([("max_line_length".to_string(), Some("abc".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("max_line_length".to_string(), Some("abc".to_string()))]),
)]),
None,
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
@@ -415,6 +462,7 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
@@ -449,7 +497,11 @@ mod tests {
#[test]
fn it_converts_plugin_options() -> Result<()> {
let actual = convert(
&HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
)]),
None,
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
@@ -464,6 +516,7 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
@@ -504,9 +557,13 @@ mod tests {
fn it_converts_docstring_conventions() -> Result<()> {
let actual = convert(
&HashMap::from([(
"docstring-convention".to_string(),
Some("numpy".to_string()),
"flake8".to_string(),
HashMap::from([(
"docstring-convention".to_string(),
Some("numpy".to_string()),
)]),
)]),
None,
Some(vec![Plugin::Flake8Docstrings]),
)?;
let expected = Pyproject::new(Options {
@@ -521,6 +578,7 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
@@ -591,7 +649,11 @@ mod tests {
#[test]
fn it_infers_plugins_if_omitted() -> Result<()> {
let actual = convert(
&HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
&HashMap::from([(
"flake8".to_string(),
HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
)]),
None,
None,
)?;
let expected = Pyproject::new(Options {
@@ -606,6 +668,7 @@ mod tests {
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,

View File

@@ -11,6 +11,7 @@
clippy::too_many_lines
)]
pub mod black;
pub mod converter;
mod parser;
pub mod plugin;

View File

@@ -17,6 +17,7 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use configparser::ini::Ini;
use flake8_to_ruff::black::parse_black_options;
use flake8_to_ruff::converter;
use flake8_to_ruff::plugin::Plugin;
@@ -26,10 +27,14 @@ use flake8_to_ruff::plugin::Plugin;
long_about = None
)]
struct Cli {
/// Path to the Flake8 configuration file (e.g., 'setup.cfg', 'tox.ini', or
/// '.flake8').
/// Path to the Flake8 configuration file (e.g., `setup.cfg`, `tox.ini`, or
/// `.flake8`).
#[arg(required = true)]
file: PathBuf,
/// Optional path to a `pyproject.toml` file, used to ensure compatibility
/// with Black.
#[arg(long)]
pyproject: Option<PathBuf>,
/// List of plugins to enable.
#[arg(long, value_delimiter = ',')]
plugin: Option<Vec<Plugin>>,
@@ -43,13 +48,15 @@ fn main() -> Result<()> {
ini.set_multiline(true);
let config = ini.load(cli.file).map_err(|msg| anyhow::anyhow!(msg))?;
// Extract the Flake8 section.
let flake8 = config
.get("flake8")
.expect("Unable to find flake8 section in INI file");
// Read the pyproject.toml file.
let black = cli
.pyproject
.map(parse_black_options)
.transpose()?
.flatten();
// Create the pyproject.toml.
let pyproject = converter::convert(flake8, cli.plugin)?;
// Create Ruff's pyproject.toml section.
let pyproject = converter::convert(&config, black.as_ref(), cli.plugin)?;
println!("{}", toml::to_string_pretty(&pyproject)?);
Ok(())

View File

@@ -3,6 +3,7 @@ use std::str::FromStr;
use anyhow::{bail, Result};
use once_cell::sync::Lazy;
use regex::Regex;
use ruff::checks::PREFIX_REDIRECTS;
use ruff::checks_gen::CheckCodePrefix;
use ruff::settings::types::PatternPrefixPair;
use rustc_hash::FxHashMap;
@@ -18,7 +19,9 @@ pub fn parse_prefix_codes(value: &str) -> Vec<CheckCodePrefix> {
if code.is_empty() {
continue;
}
if let Ok(code) = CheckCodePrefix::from_str(code) {
if let Some(code) = PREFIX_REDIRECTS.get(code) {
codes.push(code.clone());
} else if let Ok(code) = CheckCodePrefix::from_str(code) {
codes.push(code);
} else {
eprintln!("Unsupported prefix code: {code}");
@@ -83,16 +86,22 @@ impl State {
fn parse(&self) -> Vec<PatternPrefixPair> {
let mut codes: Vec<PatternPrefixPair> = vec![];
for code in &self.codes {
match CheckCodePrefix::from_str(code) {
Ok(code) => {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
prefix: code.clone(),
});
}
if let Some(code) = PREFIX_REDIRECTS.get(code.as_str()) {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
prefix: code.clone(),
});
}
Err(_) => eprintln!("Skipping unrecognized prefix: {code}"),
} else if let Ok(code) = CheckCodePrefix::from_str(code) {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
prefix: code.clone(),
});
}
} else {
eprintln!("Unsupported prefix code: {code}");
}
}
codes

View File

@@ -1,10 +1,11 @@
use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::str::FromStr;
use anyhow::anyhow;
use ruff::checks_gen::CheckCodePrefix;
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Plugin {
Flake8Annotations,
Flake8Bandit,
@@ -42,7 +43,7 @@ impl FromStr for Plugin {
"flake8-datetimez" => Ok(Plugin::Flake8Datetimez),
"flake8-debugger" => Ok(Plugin::Flake8Debugger),
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-eradicate" => Ok(Plugin::Flake8BlindExcept),
"flake8-eradicate" => Ok(Plugin::Flake8Eradicate),
"flake8-errmsg" => Ok(Plugin::Flake8ErrMsg),
"flake8-print" => Ok(Plugin::Flake8Print),
"flake8-quotes" => Ok(Plugin::Flake8Quotes),
@@ -58,6 +59,37 @@ impl FromStr for Plugin {
}
}
impl fmt::Debug for Plugin {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
Plugin::Flake8Annotations => "flake8-annotations",
Plugin::Flake8Bandit => "flake8-bandit",
Plugin::Flake8BlindExcept => "flake8-blind-except",
Plugin::Flake8Bugbear => "flake8-bugbear",
Plugin::Flake8Builtins => "flake8-builtins",
Plugin::Flake8Comprehensions => "flake8-comprehensions",
Plugin::Flake8Datetimez => "flake8-datetimez",
Plugin::Flake8Debugger => "flake8-debugger",
Plugin::Flake8Docstrings => "flake8-docstrings",
Plugin::Flake8Eradicate => "flake8-eradicate",
Plugin::Flake8ErrMsg => "flake8-errmsg",
Plugin::Flake8Print => "flake8-print",
Plugin::Flake8Quotes => "flake8-quotes",
Plugin::Flake8Return => "flake8-return",
Plugin::Flake8Simplify => "flake8-simplify",
Plugin::Flake8TidyImports => "flake8-tidy-imports",
Plugin::McCabe => "mccabe",
Plugin::PandasVet => "pandas-vet",
Plugin::PEP8Naming => "pep8-naming",
Plugin::Pyupgrade => "pyupgrade",
}
)
}
}
impl Plugin {
pub fn default(&self) -> CheckCodePrefix {
match self {
@@ -78,11 +110,11 @@ impl Plugin {
Plugin::Flake8Quotes => CheckCodePrefix::Q,
Plugin::Flake8Return => CheckCodePrefix::RET,
Plugin::Flake8Simplify => CheckCodePrefix::SIM,
Plugin::Flake8TidyImports => CheckCodePrefix::I25,
Plugin::Flake8TidyImports => CheckCodePrefix::TID25,
Plugin::McCabe => CheckCodePrefix::C9,
Plugin::PandasVet => CheckCodePrefix::PD,
Plugin::PEP8Naming => CheckCodePrefix::N,
Plugin::Pyupgrade => CheckCodePrefix::U,
Plugin::Pyupgrade => CheckCodePrefix::UP,
}
}
@@ -424,7 +456,6 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin
Plugin::Flake8TidyImports,
Plugin::PandasVet,
Plugin::PEP8Naming,
Plugin::Pyupgrade,
]
.into_iter()
.filter(|plugin| {

View File

@@ -32,6 +32,8 @@ build-backend = "maturin"
bindings = "bin"
strip = true
[tool.ruff]
[tool.ruff.isort]
force-wrap-aliases = true
combine-as-imports = true

View File

@@ -55,3 +55,5 @@ a.get("hello", False)
{}.pop(True, False)
dict.fromkeys(("world",), True)
{}.deploy(True, False)
getattr(someobj, attrname, False)
mylist.index(True)

View File

@@ -6,6 +6,9 @@ datetime.datetime(2000, 1, 1, 0, 0, 0)
# none args
datetime.datetime(2000, 1, 1, 0, 0, 0, 0, None)
# not none arg
datetime.datetime(2000, 1, 1, 0, 0, 0, 0, datetime.timezone.utc)
# no kwargs
datetime.datetime(2000, 1, 1, fold=1)

View File

@@ -23,6 +23,12 @@ datetime.datetime.strptime("something", "something").astimezone()
# OK
datetime.datetime.strptime("something", "%H:%M:%S%z")
# OK
datetime.datetime.strptime("something", something).astimezone()
# OK
datetime.datetime.strptime("something", something).replace(tzinfo=datetime.timezone.utc)
from datetime import datetime
# no replace orastimezone unqualified

View File

@@ -6,18 +6,6 @@ def x():
return a # error
def x():
b, a = 1, 2
print(b)
return a # error
def x():
a = 1
print()
return a # error
def x():
a = 1
print(a)
@@ -53,7 +41,6 @@ def x():
# https://github.com/afonasev/flake8-return/issues/47#issue-641117366
def user_agent_username(username=None):
if not username:
return ""
@@ -136,6 +123,20 @@ def x():
return a
# Considered OK, since functions can have side effects.
def x():
b, a = 1, 2
print(b)
return a
# Considered OK, since functions can have side effects.
def x():
a = 1
print()
return a
# Test cases for using value for assignment then returning it
# See:https://github.com/afonasev/flake8-return/issues/47
def resolve_from_url(self, url: str) -> dict:

View File

@@ -0,0 +1,61 @@
#: E401
import os, sys
#: Okay
import os
import sys
from subprocess import Popen, PIPE
from myclass import MyClass
from foo.bar.yourclass import YourClass
import myclass
import foo.bar.yourclass
#: Okay
__all__ = ['abc']
import foo
#: Okay
__version__ = "42"
import foo
#: Okay
__author__ = "Simon Gomizelj"
import foo
#: Okay
try:
import foo
except ImportError:
pass
else:
print('imported foo')
finally:
print('made attempt to import foo')
import bar
#: Okay
with warnings.catch_warnings():
warnings.filterwarnings("ignore", DeprecationWarning)
import foo
import bar
#: Okay
if False:
import foo
elif not True:
import bar
else:
import mwahaha
import bar
#: E402
VERSION = '1.2.3'
import foo
#: E402
import foo
a = 1
import bar

View File

@@ -0,0 +1,13 @@
"""Test: Mypy extensions."""
from mypy_extensions import DefaultNamedArg
# OK
_ = DefaultNamedArg(bool | None, name="some_prop_name")
_ = DefaultNamedArg(type=bool | None, name="some_prop_name")
_ = DefaultNamedArg(bool | None, "some_prop_name")
# Not OK
_ = DefaultNamedArg("Undefined", name="some_prop_name")
_ = DefaultNamedArg(type="Undefined", name="some_prop_name")
_ = DefaultNamedArg("Undefined", "some_prop_name")

View File

@@ -109,6 +109,11 @@ def f():
del x
def f():
print(f"{x=}")
global x
###
# Non-errors.
###
@@ -146,3 +151,8 @@ def f():
global x, y
del x
def f():
global x
print(f"{x=}")

View File

@@ -0,0 +1,11 @@
import datetime
import datetime as dt
from datetime import timezone
from datetime import timezone as tz
print(datetime.timezone(-1))
print(timezone.utc)
print(tz.utc)
print(datetime.timezone.utc)
print(dt.timezone.utc)

View File

@@ -79,3 +79,10 @@ _ = """Here's a source: https://github.com/ethereum/web3.py/blob/ffe59daf10edc19
May raise:
- DeserializationError if the abi string is invalid or abi or log topics/data do not match
""" # noqa: E501
import collections # noqa
import os # noqa: F401, RUF100
import shelve # noqa: RUF100
import sys # noqa: F401, RUF100
print(sys.path)

View File

@@ -9,14 +9,14 @@ Running from the repo root should pick up and enforce the appropriate settings f
```
∴ cargo run resources/test/project/
Found 7 error(s).
resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
resources/test/project/src/file.py:1:8: F401 `os` imported but unused
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
resources/test/project/project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
Found 7 error(s).
6 potentially fixable with the --fix option.
```
@@ -24,14 +24,14 @@ Running from the project directory itself should exhibit the same behavior:
```
∴ (cd resources/test/project/ && cargo run .)
Found 7 error(s).
examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
src/file.py:1:8: F401 `os` imported but unused
src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
project/file.py:1:8: F401 `os` imported but unused
project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
Found 7 error(s).
6 potentially fixable with the --fix option.
```
@@ -40,9 +40,9 @@ files:
```
∴ (cd resources/test/project/examples/docs && cargo run .)
Found 2 error(s).
docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
Found 2 error(s).
1 potentially fixable with the --fix option.
```
@@ -51,8 +51,6 @@ file paths from the current working directory:
```
∴ (cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/)
Found 11 error(s).
resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused
@@ -61,9 +59,9 @@ resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but un
resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused
resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
resources/test/project/src/file.py:1:8: F401 `os` imported but unused
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
11 potentially fixable with the --fix option.
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
Found 9 error(s).
9 potentially fixable with the --fix option.
```
Running from a parent directory should this "ignore" the `exclude` (hence, `concepts/file.py` gets
@@ -71,11 +69,11 @@ included in the output):
```
∴ (cd resources/test/project/examples && cargo run -- --config=docs/pyproject.toml .)
Found 4 error(s).
docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
excluded/script.py:5:5: F841 Local variable `x` is assigned to but never used
Found 4 error(s).
1 potentially fixable with the --fix option.
```
@@ -83,7 +81,14 @@ Passing an excluded directory directly should report errors in the contained fil
```
∴ cargo run resources/test/project/examples/excluded/
Found 1 error(s).
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
Found 1 error(s).
1 potentially fixable with the --fix option.
```
Unless we `--force-exclude`:
```
∴ cargo run resources/test/project/examples/excluded/ --force-exclude
∴ cargo run resources/test/project/examples/excluded/script.py --force-exclude
```

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.188"
version = "0.0.192"
edition = "2021"
[dependencies]

View File

@@ -10,7 +10,7 @@ use anyhow::{ensure, Result};
use clap::Parser;
use codegen::{Scope, Type, Variant};
use itertools::Itertools;
use ruff::checks::{CheckCode, CODE_REDIRECTS, PREFIX_REDIRECTS};
use ruff::checks::{CheckCode, PREFIX_REDIRECTS};
use strum::IntoEnumIterator;
#[derive(Parser)]
@@ -40,18 +40,7 @@ pub fn main(cli: &Cli) -> Result<()> {
}
// Add any prefix aliases (e.g., "U" to "UP").
for (alias, source) in PREFIX_REDIRECTS.iter() {
prefix_to_codes.insert(
(*alias).to_string(),
prefix_to_codes
.get(&(*source).to_string())
.unwrap_or_else(|| panic!("Unknown CheckCode: {source:?}"))
.clone(),
);
}
// Add any check code aliases (e.g., "U001" to "UP001").
for (alias, check_code) in CODE_REDIRECTS.iter() {
for (alias, check_code) in PREFIX_REDIRECTS.iter() {
prefix_to_codes.insert(
(*alias).to_string(),
prefix_to_codes
@@ -105,7 +94,7 @@ pub fn main(cli: &Cli) -> Result<()> {
.line("#[allow(clippy::match_same_arms)]")
.line("match self {");
for (prefix, codes) in &prefix_to_codes {
if let Some(target) = CODE_REDIRECTS.get(&prefix.as_str()) {
if let Some(target) = PREFIX_REDIRECTS.get(&prefix.as_str()) {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => {{ one_time_warning!(\"{{}}{{}} {{}}\", \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been remapped to \
@@ -117,18 +106,6 @@ pub fn main(cli: &Cli) -> Result<()> {
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
} else if let Some(target) = PREFIX_REDIRECTS.get(&prefix.as_str()) {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => {{ one_time_warning!(\"{{}}{{}} {{}}\", \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been remapped to \
`{}`\".bold()); \n vec![{}] }}",
prefix,
target,
codes
.iter()
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
} else {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => vec![{}],",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.188"
version = "0.0.192"
edition = "2021"
[lib]

View File

@@ -8,6 +8,7 @@ use std::path::Path;
use anyhow::Result;
use filetime::FileTime;
use log::error;
use once_cell::sync::Lazy;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
@@ -15,6 +16,7 @@ use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::{flags, Settings};
static CACHE_DIR: Lazy<Option<String>> = Lazy::new(|| std::env::var("RUFF_CACHE_DIR").ok());
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Serialize, Deserialize)]
@@ -34,12 +36,12 @@ struct CheckResult {
messages: Vec<Message>,
}
fn cache_dir() -> &'static str {
"./.ruff_cache"
fn cache_dir() -> &'static Path {
Path::new(CACHE_DIR.as_ref().map_or(".ruff_cache", String::as_str))
}
fn content_dir() -> &'static str {
"content"
fn content_dir() -> &'static Path {
Path::new("content")
}
fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: fixer::Mode) -> u64 {
@@ -53,7 +55,7 @@ fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: fixer::Mode)
/// Initialize the cache directory.
pub fn init() -> Result<()> {
let path = Path::new(cache_dir());
let path = cache_dir();
// Create the cache directories.
create_dir_all(path.join(content_dir()))?;
@@ -75,19 +77,13 @@ pub fn init() -> Result<()> {
fn write_sync(key: u64, value: &[u8]) -> Result<(), std::io::Error> {
fs::write(
Path::new(cache_dir())
.join(content_dir())
.join(format!("{key:x}")),
cache_dir().join(content_dir()).join(format!("{key:x}")),
value,
)
}
fn read_sync(key: u64) -> Result<Vec<u8>, std::io::Error> {
fs::read(
Path::new(cache_dir())
.join(content_dir())
.join(format!("{key:x}")),
)
fs::read(cache_dir().join(content_dir()).join(format!("{key:x}")))
}
/// Get a value from the cache.

View File

@@ -156,19 +156,14 @@ impl<'a> Checker<'a> {
}
/// Add a `Check` to the `Checker`.
pub(crate) fn add_check(&mut self, check: Check) {
pub(crate) fn add_check(&mut self, mut check: Check) {
// If we're in an f-string, override the location. RustPython doesn't produce
// reliable locations for expressions within f-strings, so we use the
// span of the f-string itself as a best-effort default.
let check = if let Some(range) = self.in_f_string {
Check {
location: range.location,
end_location: range.end_location,
..check
}
} else {
check
};
if let Some(range) = self.in_f_string {
check.location = range.location;
check.end_location = range.end_location;
}
self.checks.push(check);
}
@@ -189,6 +184,13 @@ impl<'a> Checker<'a> {
&& self.settings.fixable.contains(code)
}
/// Return the amended `Range` from a `Located`.
pub fn range_for<T>(&self, located: &Located<T>) -> Range {
// If we're in an f-string, override the location.
self.in_f_string
.unwrap_or_else(|| Range::from_located(located))
}
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
pub fn match_typing_expr(&self, expr: &Expr, target: &str) -> bool {
let call_path = dealias_call_path(collect_call_paths(expr), &self.import_aliases);
@@ -626,6 +628,15 @@ where
}
}
StmtKind::Import { names } => {
if self.settings.enabled.contains(&CheckCode::E401) {
if names.len() > 1 {
self.add_check(Check::new(
CheckKind::MultipleImportsOnOneLine,
Range::from_located(stmt),
));
}
}
if self.settings.enabled.contains(&CheckCode::E402) {
if self.seen_import_boundary && stmt.location.column() == 0 {
self.add_check(Check::new(
@@ -1542,6 +1553,12 @@ where
pyupgrade::plugins::remove_six_compat(self, expr);
}
if self.settings.enabled.contains(&CheckCode::UP017)
&& self.settings.target_version >= PythonVersion::Py311
{
pyupgrade::plugins::datetime_utc_alias(self, expr);
}
if self.settings.enabled.contains(&CheckCode::YTT202) {
flake8_2020::plugins::name_or_attribute(self, expr);
}
@@ -2507,6 +2524,29 @@ where
self.visit_expr(value);
self.in_type_definition = prev_in_type_definition;
}
} else if ["Arg", "DefaultArg", "NamedArg", "DefaultNamedArg"]
.iter()
.any(|target| {
match_call_path(&call_path, "mypy_extensions", target, &self.from_imports)
})
{
self.visit_expr(func);
// Ex) DefaultNamedArg(bool | None, name="some_prop_name")
let mut arguments = args.iter().chain(keywords.iter().map(|keyword| {
let KeywordData { value, .. } = &keyword.node;
value
}));
if let Some(expr) = arguments.next() {
self.in_type_definition = true;
self.visit_expr(expr);
self.in_type_definition = prev_in_type_definition;
}
for expr in arguments {
self.in_type_definition = false;
self.visit_expr(expr);
self.in_type_definition = prev_in_type_definition;
}
} else {
visitor::walk_expr(self, expr);
}

View File

@@ -100,18 +100,32 @@ pub fn check_noqa(
Directive::Codes(spaces, start, end, codes) => {
let mut invalid_codes = vec![];
let mut valid_codes = vec![];
let mut self_ignore = false;
for code in codes {
let code = CODE_REDIRECTS.get(code).map_or(code, AsRef::as_ref);
if matches.contains(&code) || settings.external.contains(code) {
valid_codes.push(code.to_string());
if code == CheckCode::RUF100.as_ref() {
self_ignore = true;
} else {
invalid_codes.push(code.to_string());
if matches.contains(&code) || settings.external.contains(code) {
valid_codes.push(code);
} else {
invalid_codes.push(code);
}
}
}
if self_ignore {
continue;
}
if !invalid_codes.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(Some(invalid_codes)),
CheckKind::UnusedNOQA(Some(
invalid_codes
.iter()
.map(|code| (*code).to_string())
.collect(),
)),
Range {
location: Location::new(row + 1, start),
end_location: Location::new(row + 1, end),

View File

@@ -33,6 +33,7 @@ use crate::pyupgrade::types::Primitive;
)]
pub enum CheckCode {
// pycodestyle errors
E401,
E402,
E501,
E711,
@@ -224,6 +225,7 @@ pub enum CheckCode {
UP014,
UP015,
UP016,
UP017,
// pydocstyle
D100,
D101,
@@ -644,6 +646,7 @@ pub enum CheckKind {
IOError(String),
LineTooLong(usize, usize),
ModuleImportNotAtTopOfFile,
MultipleImportsOnOneLine,
NoneComparison(RejectedCmpop),
NotInTest,
NotIsTest,
@@ -825,6 +828,7 @@ pub enum CheckKind {
ConvertNamedTupleFunctionalToClass(String),
RedundantOpenModes,
RemoveSixCompat,
DatetimeTimezoneUTC,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -978,6 +982,7 @@ impl CheckCode {
pub fn kind(&self) -> CheckKind {
match self {
// pycodestyle errors
CheckCode::E401 => CheckKind::MultipleImportsOnOneLine,
CheckCode::E402 => CheckKind::ModuleImportNotAtTopOfFile,
CheckCode::E501 => CheckKind::LineTooLong(89, 88),
CheckCode::E711 => CheckKind::NoneComparison(RejectedCmpop::Eq),
@@ -1197,6 +1202,7 @@ impl CheckCode {
CheckCode::UP014 => CheckKind::ConvertNamedTupleFunctionalToClass("...".to_string()),
CheckCode::UP015 => CheckKind::RedundantOpenModes,
CheckCode::UP016 => CheckKind::RemoveSixCompat,
CheckCode::UP017 => CheckKind::DatetimeTimezoneUTC,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -1462,6 +1468,7 @@ impl CheckCode {
CheckCode::DTZ007 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ011 => CheckCategory::Flake8Datetimez,
CheckCode::DTZ012 => CheckCategory::Flake8Datetimez,
CheckCode::E401 => CheckCategory::Pycodestyle,
CheckCode::E402 => CheckCategory::Pycodestyle,
CheckCode::E501 => CheckCategory::Pycodestyle,
CheckCode::E711 => CheckCategory::Pycodestyle,
@@ -1613,6 +1620,7 @@ impl CheckCode {
CheckCode::UP014 => CheckCategory::Pyupgrade,
CheckCode::UP015 => CheckCategory::Pyupgrade,
CheckCode::UP016 => CheckCategory::Pyupgrade,
CheckCode::UP017 => CheckCategory::Pyupgrade,
CheckCode::W292 => CheckCategory::Pycodestyle,
CheckCode::W605 => CheckCategory::Pycodestyle,
CheckCode::YTT101 => CheckCategory::Flake82020,
@@ -1657,6 +1665,7 @@ impl CheckKind {
CheckKind::IsLiteral => &CheckCode::F632,
CheckKind::LateFutureImport => &CheckCode::F404,
CheckKind::LineTooLong(..) => &CheckCode::E501,
CheckKind::MultipleImportsOnOneLine => &CheckCode::E401,
CheckKind::ModuleImportNotAtTopOfFile => &CheckCode::E402,
CheckKind::MultiValueRepeatedKeyLiteral => &CheckCode::F601,
CheckKind::MultiValueRepeatedKeyVariable(_) => &CheckCode::F602,
@@ -1822,6 +1831,7 @@ impl CheckKind {
CheckKind::ConvertNamedTupleFunctionalToClass(_) => &CheckCode::UP014,
CheckKind::RedundantOpenModes => &CheckCode::UP015,
CheckKind::RemoveSixCompat => &CheckCode::UP016,
CheckKind::DatetimeTimezoneUTC => &CheckCode::UP017,
// pydocstyle
CheckKind::BlankLineAfterLastSection(_) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(_) => &CheckCode::D410,
@@ -2016,6 +2026,7 @@ impl CheckKind {
CheckKind::ModuleImportNotAtTopOfFile => {
"Module level import not at top of file".to_string()
}
CheckKind::MultipleImportsOnOneLine => "Multiple imports on one line".to_string(),
CheckKind::MultiValueRepeatedKeyLiteral => {
"Dictionary key literal repeated".to_string()
}
@@ -2543,6 +2554,7 @@ impl CheckKind {
CheckKind::UnnecessaryEncodeUTF8 => "Unnecessary call to `encode` as UTF-8".to_string(),
CheckKind::RedundantOpenModes => "Unnecessary open mode parameters".to_string(),
CheckKind::RemoveSixCompat => "Unnecessary `six` compatibility usage".to_string(),
CheckKind::DatetimeTimezoneUTC => "Use `datetime.UTC` alias".to_string(),
CheckKind::ConvertTypedDictFunctionalToClass(name) => {
format!("Convert `{name}` from `TypedDict` functional to class syntax")
}
@@ -2979,6 +2991,7 @@ impl CheckKind {
| CheckKind::RedundantOpenModes
| CheckKind::RedundantTupleInExceptionHandler(..)
| CheckKind::RemoveSixCompat
| CheckKind::DatetimeTimezoneUTC
| CheckKind::SectionNameEndsInColon(..)
| CheckKind::SectionNotOverIndented(..)
| CheckKind::SectionUnderlineAfterName(..)
@@ -3043,6 +3056,81 @@ impl Check {
}
}
/// A hash map from deprecated `CheckCodePrefix` to latest `CheckCodePrefix`.
pub static PREFIX_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCodePrefix>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U001", CheckCodePrefix::UP001),
("U003", CheckCodePrefix::UP003),
("U004", CheckCodePrefix::UP004),
("U005", CheckCodePrefix::UP005),
("U006", CheckCodePrefix::UP006),
("U007", CheckCodePrefix::UP007),
("U008", CheckCodePrefix::UP008),
("U009", CheckCodePrefix::UP009),
("U010", CheckCodePrefix::UP010),
("U011", CheckCodePrefix::UP011),
("U012", CheckCodePrefix::UP012),
("U013", CheckCodePrefix::UP013),
("U014", CheckCodePrefix::UP014),
("U015", CheckCodePrefix::UP015),
("U016", CheckCodePrefix::UP016),
("U017", CheckCodePrefix::UP017),
// TODO(charlie): Remove by 2023-02-01.
("I252", CheckCodePrefix::TID252),
("M001", CheckCodePrefix::RUF100),
// TODO(charlie): Remove by 2023-02-01.
("PDV002", CheckCodePrefix::PD002),
("PDV003", CheckCodePrefix::PD003),
("PDV004", CheckCodePrefix::PD004),
("PDV007", CheckCodePrefix::PD007),
("PDV008", CheckCodePrefix::PD008),
("PDV009", CheckCodePrefix::PD009),
("PDV010", CheckCodePrefix::PD010),
("PDV011", CheckCodePrefix::PD011),
("PDV012", CheckCodePrefix::PD012),
("PDV013", CheckCodePrefix::PD013),
("PDV015", CheckCodePrefix::PD015),
("PDV901", CheckCodePrefix::PD901),
// TODO(charlie): Remove by 2023-02-01.
("R501", CheckCodePrefix::RET501),
("R502", CheckCodePrefix::RET502),
("R503", CheckCodePrefix::RET503),
("R504", CheckCodePrefix::RET504),
("R505", CheckCodePrefix::RET505),
("R506", CheckCodePrefix::RET506),
("R507", CheckCodePrefix::RET507),
("R508", CheckCodePrefix::RET508),
("IC001", CheckCodePrefix::ICN001),
("IC002", CheckCodePrefix::ICN001),
("IC003", CheckCodePrefix::ICN001),
("IC004", CheckCodePrefix::ICN001),
// TODO(charlie): Remove by 2023-01-01.
("U", CheckCodePrefix::UP),
("U0", CheckCodePrefix::UP0),
("U00", CheckCodePrefix::UP00),
("U01", CheckCodePrefix::UP01),
// TODO(charlie): Remove by 2023-02-01.
("I2", CheckCodePrefix::TID2),
("I25", CheckCodePrefix::TID25),
("M", CheckCodePrefix::RUF100),
("M0", CheckCodePrefix::RUF100),
// TODO(charlie): Remove by 2023-02-01.
("PDV", CheckCodePrefix::PD),
("PDV0", CheckCodePrefix::PD0),
("PDV01", CheckCodePrefix::PD01),
("PDV9", CheckCodePrefix::PD9),
("PDV90", CheckCodePrefix::PD90),
// TODO(charlie): Remove by 2023-02-01.
("R", CheckCodePrefix::RET),
("R5", CheckCodePrefix::RET5),
("R50", CheckCodePrefix::RET50),
// TODO(charlie): Remove by 2023-02-01.
("IC", CheckCodePrefix::ICN),
("IC0", CheckCodePrefix::ICN0),
])
});
/// A hash map from deprecated to latest `CheckCode`.
pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(|| {
FxHashMap::from_iter([
@@ -3062,6 +3150,7 @@ pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(
("U014", CheckCode::UP014),
("U015", CheckCode::UP015),
("U016", CheckCode::UP016),
("U017", CheckCode::UP017),
// TODO(charlie): Remove by 2023-02-01.
("I252", CheckCode::TID252),
("M001", CheckCode::RUF100),
@@ -3078,28 +3167,20 @@ pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(
("PDV013", CheckCode::PD013),
("PDV015", CheckCode::PD015),
("PDV901", CheckCode::PD901),
])
});
/// A hash map from deprecated `CheckCodePrefix` to latest `CheckCodePrefix`.
pub static PREFIX_REDIRECTS: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U", "UP"),
("U0", "UP0"),
("U00", "UP00"),
("U01", "UP01"),
// TODO(charlie): Remove by 2023-02-01.
("I2", "TID2"),
("I25", "TID25"),
("M", "RUF100"),
("M0", "RUF100"),
("R501", CheckCode::RET501),
("R502", CheckCode::RET502),
("R503", CheckCode::RET503),
("R504", CheckCode::RET504),
("R505", CheckCode::RET505),
("R506", CheckCode::RET506),
("R507", CheckCode::RET507),
("R508", CheckCode::RET508),
// TODO(charlie): Remove by 2023-02-01.
("PDV", "PD"),
("PDV0", "PD0"),
("PDV01", "PD01"),
("PDV9", "PD9"),
("PDV90", "PD90"),
("IC001", CheckCode::ICN001),
("IC002", CheckCode::ICN001),
("IC003", CheckCode::ICN001),
("IC004", CheckCode::ICN001),
])
});

View File

@@ -179,6 +179,7 @@ pub enum CheckCodePrefix {
E,
E4,
E40,
E401,
E402,
E5,
E50,
@@ -291,6 +292,12 @@ pub enum CheckCodePrefix {
I2,
I25,
I252,
IC,
IC0,
IC001,
IC002,
IC003,
IC004,
ICN,
ICN0,
ICN00,
@@ -410,6 +417,17 @@ pub enum CheckCodePrefix {
Q001,
Q002,
Q003,
R,
R5,
R50,
R501,
R502,
R503,
R504,
R505,
R506,
R507,
R508,
RET,
RET5,
RET50,
@@ -474,6 +492,7 @@ pub enum CheckCodePrefix {
U014,
U015,
U016,
U017,
UP,
UP0,
UP00,
@@ -493,6 +512,7 @@ pub enum CheckCodePrefix {
UP014,
UP015,
UP016,
UP017,
W,
W2,
W29,
@@ -1046,6 +1066,7 @@ impl CheckCodePrefix {
CheckCodePrefix::DTZ011 => vec![CheckCode::DTZ011],
CheckCodePrefix::DTZ012 => vec![CheckCode::DTZ012],
CheckCodePrefix::E => vec![
CheckCode::E401,
CheckCode::E402,
CheckCode::E501,
CheckCode::E711,
@@ -1061,8 +1082,9 @@ impl CheckCodePrefix {
CheckCode::E902,
CheckCode::E999,
],
CheckCodePrefix::E4 => vec![CheckCode::E402],
CheckCodePrefix::E40 => vec![CheckCode::E402],
CheckCodePrefix::E4 => vec![CheckCode::E401, CheckCode::E402],
CheckCodePrefix::E40 => vec![CheckCode::E401, CheckCode::E402],
CheckCodePrefix::E401 => vec![CheckCode::E401],
CheckCodePrefix::E402 => vec![CheckCode::E402],
CheckCodePrefix::E5 => vec![CheckCode::E501],
CheckCodePrefix::E50 => vec![CheckCode::E501],
@@ -1343,6 +1365,60 @@ impl CheckCodePrefix {
);
vec![CheckCode::TID252]
}
CheckCodePrefix::IC => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`IC` has been remapped to `ICN`".bold()
);
vec![CheckCode::ICN001]
}
CheckCodePrefix::IC0 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`IC0` has been remapped to `ICN0`".bold()
);
vec![CheckCode::ICN001]
}
CheckCodePrefix::IC001 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`IC001` has been remapped to `ICN001`".bold()
);
vec![CheckCode::ICN001]
}
CheckCodePrefix::IC002 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`IC002` has been remapped to `ICN001`".bold()
);
vec![CheckCode::ICN001]
}
CheckCodePrefix::IC003 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`IC003` has been remapped to `ICN001`".bold()
);
vec![CheckCode::ICN001]
}
CheckCodePrefix::IC004 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`IC004` has been remapped to `ICN001`".bold()
);
vec![CheckCode::ICN001]
}
CheckCodePrefix::ICN => vec![CheckCode::ICN001],
CheckCodePrefix::ICN0 => vec![CheckCode::ICN001],
CheckCodePrefix::ICN00 => vec![CheckCode::ICN001],
@@ -1764,6 +1840,132 @@ impl CheckCodePrefix {
CheckCodePrefix::Q001 => vec![CheckCode::Q001],
CheckCodePrefix::Q002 => vec![CheckCode::Q002],
CheckCodePrefix::Q003 => vec![CheckCode::Q003],
CheckCodePrefix::R => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R` has been remapped to `RET`".bold()
);
vec![
CheckCode::RET501,
CheckCode::RET502,
CheckCode::RET503,
CheckCode::RET504,
CheckCode::RET505,
CheckCode::RET506,
CheckCode::RET507,
CheckCode::RET508,
]
}
CheckCodePrefix::R5 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R5` has been remapped to `RET5`".bold()
);
vec![
CheckCode::RET501,
CheckCode::RET502,
CheckCode::RET503,
CheckCode::RET504,
CheckCode::RET505,
CheckCode::RET506,
CheckCode::RET507,
CheckCode::RET508,
]
}
CheckCodePrefix::R50 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R50` has been remapped to `RET50`".bold()
);
vec![
CheckCode::RET501,
CheckCode::RET502,
CheckCode::RET503,
CheckCode::RET504,
CheckCode::RET505,
CheckCode::RET506,
CheckCode::RET507,
CheckCode::RET508,
]
}
CheckCodePrefix::R501 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R501` has been remapped to `RET501`".bold()
);
vec![CheckCode::RET501]
}
CheckCodePrefix::R502 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R502` has been remapped to `RET502`".bold()
);
vec![CheckCode::RET502]
}
CheckCodePrefix::R503 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R503` has been remapped to `RET503`".bold()
);
vec![CheckCode::RET503]
}
CheckCodePrefix::R504 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R504` has been remapped to `RET504`".bold()
);
vec![CheckCode::RET504]
}
CheckCodePrefix::R505 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R505` has been remapped to `RET505`".bold()
);
vec![CheckCode::RET505]
}
CheckCodePrefix::R506 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R506` has been remapped to `RET506`".bold()
);
vec![CheckCode::RET506]
}
CheckCodePrefix::R507 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R507` has been remapped to `RET507`".bold()
);
vec![CheckCode::RET507]
}
CheckCodePrefix::R508 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`R508` has been remapped to `RET508`".bold()
);
vec![CheckCode::RET508]
}
CheckCodePrefix::RET => vec![
CheckCode::RET501,
CheckCode::RET502,
@@ -1885,6 +2087,7 @@ impl CheckCodePrefix {
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
]
}
CheckCodePrefix::U0 => {
@@ -1910,6 +2113,7 @@ impl CheckCodePrefix {
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
]
}
CheckCodePrefix::U00 => {
@@ -2017,6 +2221,7 @@ impl CheckCodePrefix {
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
]
}
CheckCodePrefix::U010 => {
@@ -2082,6 +2287,15 @@ impl CheckCodePrefix {
);
vec![CheckCode::UP016]
}
CheckCodePrefix::U017 => {
one_time_warning!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U017` has been remapped to `UP017`".bold()
);
vec![CheckCode::UP017]
}
CheckCodePrefix::UP => vec![
CheckCode::UP001,
CheckCode::UP003,
@@ -2098,6 +2312,7 @@ impl CheckCodePrefix {
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
],
CheckCodePrefix::UP0 => vec![
CheckCode::UP001,
@@ -2115,6 +2330,7 @@ impl CheckCodePrefix {
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
],
CheckCodePrefix::UP00 => vec![
CheckCode::UP001,
@@ -2142,6 +2358,7 @@ impl CheckCodePrefix {
CheckCode::UP014,
CheckCode::UP015,
CheckCode::UP016,
CheckCode::UP017,
],
CheckCodePrefix::UP010 => vec![CheckCode::UP010],
CheckCodePrefix::UP011 => vec![CheckCode::UP011],
@@ -2150,6 +2367,7 @@ impl CheckCodePrefix {
CheckCodePrefix::UP014 => vec![CheckCode::UP014],
CheckCodePrefix::UP015 => vec![CheckCode::UP015],
CheckCodePrefix::UP016 => vec![CheckCode::UP016],
CheckCodePrefix::UP017 => vec![CheckCode::UP017],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -2371,6 +2589,7 @@ impl CheckCodePrefix {
CheckCodePrefix::E => SuffixLength::Zero,
CheckCodePrefix::E4 => SuffixLength::One,
CheckCodePrefix::E40 => SuffixLength::Two,
CheckCodePrefix::E401 => SuffixLength::Three,
CheckCodePrefix::E402 => SuffixLength::Three,
CheckCodePrefix::E5 => SuffixLength::One,
CheckCodePrefix::E50 => SuffixLength::Two,
@@ -2483,6 +2702,12 @@ impl CheckCodePrefix {
CheckCodePrefix::I2 => SuffixLength::One,
CheckCodePrefix::I25 => SuffixLength::Two,
CheckCodePrefix::I252 => SuffixLength::Three,
CheckCodePrefix::IC => SuffixLength::Zero,
CheckCodePrefix::IC0 => SuffixLength::One,
CheckCodePrefix::IC001 => SuffixLength::Three,
CheckCodePrefix::IC002 => SuffixLength::Three,
CheckCodePrefix::IC003 => SuffixLength::Three,
CheckCodePrefix::IC004 => SuffixLength::Three,
CheckCodePrefix::ICN => SuffixLength::Zero,
CheckCodePrefix::ICN0 => SuffixLength::One,
CheckCodePrefix::ICN00 => SuffixLength::Two,
@@ -2602,6 +2827,17 @@ impl CheckCodePrefix {
CheckCodePrefix::Q001 => SuffixLength::Three,
CheckCodePrefix::Q002 => SuffixLength::Three,
CheckCodePrefix::Q003 => SuffixLength::Three,
CheckCodePrefix::R => SuffixLength::Zero,
CheckCodePrefix::R5 => SuffixLength::One,
CheckCodePrefix::R50 => SuffixLength::Two,
CheckCodePrefix::R501 => SuffixLength::Three,
CheckCodePrefix::R502 => SuffixLength::Three,
CheckCodePrefix::R503 => SuffixLength::Three,
CheckCodePrefix::R504 => SuffixLength::Three,
CheckCodePrefix::R505 => SuffixLength::Three,
CheckCodePrefix::R506 => SuffixLength::Three,
CheckCodePrefix::R507 => SuffixLength::Three,
CheckCodePrefix::R508 => SuffixLength::Three,
CheckCodePrefix::RET => SuffixLength::Zero,
CheckCodePrefix::RET5 => SuffixLength::One,
CheckCodePrefix::RET50 => SuffixLength::Two,
@@ -2666,6 +2902,7 @@ impl CheckCodePrefix {
CheckCodePrefix::U014 => SuffixLength::Three,
CheckCodePrefix::U015 => SuffixLength::Three,
CheckCodePrefix::U016 => SuffixLength::Three,
CheckCodePrefix::U017 => SuffixLength::Three,
CheckCodePrefix::UP => SuffixLength::Zero,
CheckCodePrefix::UP0 => SuffixLength::One,
CheckCodePrefix::UP00 => SuffixLength::Two,
@@ -2685,6 +2922,7 @@ impl CheckCodePrefix {
CheckCodePrefix::UP014 => SuffixLength::Three,
CheckCodePrefix::UP015 => SuffixLength::Three,
CheckCodePrefix::UP016 => SuffixLength::Three,
CheckCodePrefix::UP017 => SuffixLength::Three,
CheckCodePrefix::W => SuffixLength::Zero,
CheckCodePrefix::W2 => SuffixLength::One,
CheckCodePrefix::W29 => SuffixLength::Two,

View File

@@ -92,6 +92,12 @@ pub struct Cli {
respect_gitignore: bool,
#[clap(long, overrides_with("respect_gitignore"), hide = true)]
no_respect_gitignore: bool,
/// Enforce exclusions, even for paths passed to Ruff directly on the
/// command-line.
#[arg(long, overrides_with("no_show_source"))]
force_exclude: bool,
#[clap(long, overrides_with("force_exclude"), hide = true)]
no_force_exclude: bool,
/// See the files Ruff will be run against with the current settings.
#[arg(long)]
pub show_files: bool,
@@ -173,6 +179,7 @@ impl Cli {
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
fix: resolve_bool_arg(self.fix, self.no_fix),
format: self.format,
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
},
)
}
@@ -230,6 +237,7 @@ pub struct Overrides {
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
pub fix: Option<bool>,
pub format: Option<SerializationFormat>,
pub force_exclude: Option<bool>,
}
/// Map the CLI settings to a `LogLevel`.

View File

@@ -114,16 +114,26 @@ fn read_from_stdin() -> Result<String> {
/// Run the linter over a single file, read from `stdin`.
pub fn run_stdin(
strategy: &PyprojectDiscovery,
filename: &Path,
filename: Option<&Path>,
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
autofix: fixer::Mode,
) -> Result<Diagnostics> {
let stdin = read_from_stdin()?;
let settings = match strategy {
if let Some(filename) = filename {
if !resolver::python_file_at_path(filename, pyproject_strategy, file_strategy, overrides)? {
return Ok(Diagnostics::default());
}
}
let settings = match pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings,
PyprojectDiscovery::Hierarchical(settings) => settings,
};
let mut diagnostics = lint_stdin(filename, &stdin, settings, autofix)?;
let package_root = filename
.and_then(Path::parent)
.and_then(packages::detect_package_root);
let stdin = read_from_stdin()?;
let mut diagnostics = lint_stdin(filename, package_root, &stdin, settings, autofix)?;
diagnostics.messages.sort_unstable();
Ok(diagnostics)
}

View File

@@ -5,16 +5,35 @@ use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
const FUNC_NAME_ALLOWLIST: &[&str] = &["get", "setdefault", "pop", "fromkeys"];
const FUNC_NAME_ALLOWLIST: &[&str] = &[
"assertEqual",
"assertEquals",
"assertNotEqual",
"assertNotEquals",
"failIfEqual",
"failUnlessEqual",
"fromkeys",
"get",
"getattr",
"index",
"pop",
"setattr",
"setdefault",
];
/// Returns `true` if an argument is allowed to use a boolean trap. To return
/// `true`, the function name must be explicitly allowed, and the argument must
/// be either the first or second argument in the call.
fn allow_boolean_trap(func: &Expr) -> bool {
let ExprKind::Attribute { attr, .. } = &func.node else {
return false;
};
FUNC_NAME_ALLOWLIST.contains(&attr.as_ref())
if let ExprKind::Attribute { attr, .. } = &func.node {
return FUNC_NAME_ALLOWLIST.contains(&attr.as_ref());
}
if let ExprKind::Name { id, .. } = &func.node {
return FUNC_NAME_ALLOWLIST.contains(&id.as_ref());
}
false
}
fn is_boolean_arg(arg: &Expr) -> bool {
@@ -79,8 +98,8 @@ pub fn check_boolean_positional_value_in_function_call(
args: &[Expr],
func: &Expr,
) {
for (index, arg) in args.iter().enumerate() {
if index < 2 && allow_boolean_trap(func) {
for arg in args {
if allow_boolean_trap(func) {
continue;
}
add_if_boolean(

View File

@@ -19,20 +19,14 @@ pub fn call_datetime_without_tzinfo(
return;
}
// no args / no args unqualified
if args.len() < 8 && keywords.is_empty() {
// No positional arg: keyword is missing or constant None.
if args.len() < 8 && !has_non_none_keyword(keywords, "tzinfo") {
checker.add_check(Check::new(CheckKind::CallDatetimeWithoutTzinfo, location));
return;
}
// none args
if args.len() == 8 && is_const_none(&args[7]) {
checker.add_check(Check::new(CheckKind::CallDatetimeWithoutTzinfo, location));
return;
}
// no kwargs / none kwargs
if !has_non_none_keyword(keywords, "tzinfo") {
// Positional arg: is constant None.
if args.len() >= 8 && is_const_none(&args[7]) {
checker.add_check(Check::new(CheckKind::CallDatetimeWithoutTzinfo, location));
}
}
@@ -177,22 +171,17 @@ pub fn call_datetime_strptime_without_zone(
return;
}
let Some(ExprKind::Constant {
// Does the `strptime` call contain a format string with a timezone specifier?
if let Some(ExprKind::Constant {
value: Constant::Str(format),
kind: None,
}) = args.get(1).as_ref().map(|arg| &arg.node) else {
checker.add_check(Check::new(
CheckKind::CallDatetimeStrptimeWithoutZone,
location,
));
return;
}) = args.get(1).as_ref().map(|arg| &arg.node)
{
if format.contains("%z") {
return;
}
};
// Does the `strptime` call contain a format string with a timezone specifier?
if format.contains("%z") {
return;
}
let (Some(grandparent), Some(parent)) = (checker.current_expr_grandparent(), checker.current_expr_parent()) else {
checker.add_check(Check::new(
CheckKind::CallDatetimeStrptimeWithoutZone,

View File

@@ -20,26 +20,26 @@ expression: checks
fix: ~
- kind: CallDatetimeWithoutTzinfo
location:
row: 10
row: 13
column: 0
end_location:
row: 10
row: 13
column: 37
fix: ~
- kind: CallDatetimeWithoutTzinfo
location:
row: 13
row: 16
column: 0
end_location:
row: 13
row: 16
column: 42
fix: ~
- kind: CallDatetimeWithoutTzinfo
location:
row: 18
row: 21
column: 0
end_location:
row: 18
row: 21
column: 29
fix: ~

View File

@@ -36,10 +36,10 @@ expression: checks
fix: ~
- kind: CallDatetimeStrptimeWithoutZone
location:
row: 29
row: 35
column: 0
end_location:
row: 29
row: 35
column: 43
fix: ~

View File

@@ -12,50 +12,34 @@ expression: checks
fix: ~
- kind: UnnecessaryAssign
location:
row: 12
row: 13
column: 11
end_location:
row: 12
row: 13
column: 12
fix: ~
- kind: UnnecessaryAssign
location:
row: 18
column: 11
end_location:
row: 18
column: 12
fix: ~
- kind: UnnecessaryAssign
location:
row: 25
column: 11
end_location:
row: 25
column: 12
fix: ~
- kind: UnnecessaryAssign
location:
row: 31
row: 19
column: 15
end_location:
row: 31
row: 19
column: 16
fix: ~
- kind: UnnecessaryAssign
location:
row: 43
row: 31
column: 11
end_location:
row: 43
row: 31
column: 17
fix: ~
- kind: UnnecessaryAssign
location:
row: 51
row: 39
column: 11
end_location:
row: 51
row: 39
column: 20
fix: ~

View File

@@ -100,6 +100,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
.push((stmt.location, stmt.end_location.unwrap()));
visitor::walk_stmt(self, stmt);
}
_ => {
visitor::walk_stmt(self, stmt);
}
@@ -108,6 +109,17 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
fn visit_expr(&mut self, expr: &'a Expr) {
match &expr.node {
ExprKind::Call { .. } => {
// Arbitrary function calls can have side effects, so we conservatively treat
// every function call as a reference to every known variable.
for name in self.stack.assigns.keys() {
self.stack
.refs
.entry(name)
.or_insert_with(Vec::new)
.push(self.in_f_string.unwrap_or(expr.location));
}
}
ExprKind::Name { id, .. } => {
self.stack
.refs

View File

@@ -260,7 +260,8 @@ pub fn autoformat_path(path: &Path, _settings: &Settings) -> Result<()> {
/// Generate a list of `Check` violations from source code content derived from
/// stdin.
pub fn lint_stdin(
path: &Path,
path: Option<&Path>,
package: Option<&Path>,
stdin: &str,
settings: &Settings,
autofix: fixer::Mode,
@@ -269,7 +270,13 @@ pub fn lint_stdin(
let contents = stdin.to_string();
// Lint the file.
let (contents, fixed, messages) = lint(contents, path, None, settings, autofix)?;
let (contents, fixed, messages) = lint(
contents,
path.unwrap_or_else(|| Path::new("-")),
package,
settings,
autofix,
)?;
// Write the fixed contents to stdout.
if matches!(autofix, fixer::Mode::Apply) {

View File

@@ -103,6 +103,10 @@ fn inner_main() -> Result<ExitCode> {
// Extract options that are included in `Settings`, but only apply at the top
// level.
let file_strategy = FileDiscovery {
force_exclude: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.force_exclude,
PyprojectDiscovery::Hierarchical(settings) => settings.force_exclude,
},
respect_gitignore: match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => settings.respect_gitignore,
PyprojectDiscovery::Hierarchical(settings) => settings.respect_gitignore,
@@ -220,8 +224,13 @@ fn inner_main() -> Result<ExitCode> {
// Generate lint violations.
let diagnostics = if is_stdin {
let path = cli.stdin_filename.unwrap_or_else(|| PathBuf::from("-"));
commands::run_stdin(&pyproject_strategy, &path, autofix)?
commands::run_stdin(
cli.stdin_filename.as_deref(),
&pyproject_strategy,
&file_strategy,
&overrides,
autofix,
)?
} else {
commands::run(
&cli.files,

View File

@@ -44,7 +44,7 @@ impl<'a> Printer<'a> {
}
}
fn pre_text(&self, diagnostics: &Diagnostics) {
fn post_text(&self, diagnostics: &Diagnostics, autofix: fixer::Mode) {
if self.log_level >= &LogLevel::Default {
let fixed = diagnostics.fixed;
let remaining = diagnostics.messages.len();
@@ -54,13 +54,16 @@ impl<'a> Printer<'a> {
} else if remaining > 0 {
println!("Found {remaining} error(s).");
}
}
}
fn post_text(&self, num_fixable: usize, autofix: fixer::Mode) {
if self.log_level >= &LogLevel::Default {
if num_fixable > 0 && !matches!(autofix, fixer::Mode::Apply) {
println!("{num_fixable} potentially fixable with the --fix option.");
if !matches!(autofix, fixer::Mode::Apply) {
let num_fixable = diagnostics
.messages
.iter()
.filter(|message| message.kind.fixable())
.count();
if num_fixable > 0 {
println!("{num_fixable} potentially fixable with the --fix option.");
}
}
}
}
@@ -70,12 +73,6 @@ impl<'a> Printer<'a> {
return Ok(());
}
let num_fixable = diagnostics
.messages
.iter()
.filter(|message| message.kind.fixable())
.count();
match self.format {
SerializationFormat::Json => {
println!(
@@ -141,18 +138,13 @@ impl<'a> Printer<'a> {
println!("{}", report.to_string().unwrap());
}
SerializationFormat::Text => {
self.pre_text(diagnostics);
for message in &diagnostics.messages {
print_message(message);
}
self.post_text(num_fixable, autofix);
self.post_text(diagnostics, autofix);
}
SerializationFormat::Grouped => {
self.pre_text(diagnostics);
println!();
// Group by filename.
let mut grouped_messages = BTreeMap::default();
for message in &diagnostics.messages {
@@ -190,11 +182,9 @@ impl<'a> Printer<'a> {
println!();
}
self.post_text(num_fixable, autofix);
self.post_text(diagnostics, autofix);
}
SerializationFormat::Github => {
self.pre_text(diagnostics);
// Generate error workflow command in GitHub Actions format
// https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
diagnostics.messages.iter().for_each(|message| {

View File

@@ -13,24 +13,26 @@ mod tests {
use crate::linter::test_path;
use crate::settings;
#[test_case(CheckCode::E402, Path::new("E402.py"); "E402")]
#[test_case(CheckCode::E501, Path::new("E501.py"); "E501")]
#[test_case(CheckCode::E711, Path::new("E711.py"); "E711")]
#[test_case(CheckCode::E712, Path::new("E712.py"); "E712")]
#[test_case(CheckCode::E713, Path::new("E713.py"); "E713")]
#[test_case(CheckCode::E714, Path::new("E714.py"); "E714")]
#[test_case(CheckCode::E721, Path::new("E721.py"); "E721")]
#[test_case(CheckCode::E722, Path::new("E722.py"); "E722")]
#[test_case(CheckCode::E731, Path::new("E731.py"); "E731")]
#[test_case(CheckCode::E741, Path::new("E741.py"); "E741")]
#[test_case(CheckCode::E742, Path::new("E742.py"); "E742")]
#[test_case(CheckCode::E743, Path::new("E743.py"); "E743")]
#[test_case(CheckCode::E999, Path::new("E999.py"); "E999")]
#[test_case(CheckCode::W292, Path::new("W292_0.py"); "W292_0")]
#[test_case(CheckCode::W292, Path::new("W292_1.py"); "W292_1")]
#[test_case(CheckCode::W292, Path::new("W292_2.py"); "W292_2")]
#[test_case(CheckCode::W605, Path::new("W605_0.py"); "W605_0")]
#[test_case(CheckCode::W605, Path::new("W605_1.py"); "W605_1")]
#[test_case(CheckCode::E401, Path::new("E40.py"))]
#[test_case(CheckCode::E402, Path::new("E40.py"))]
#[test_case(CheckCode::E402, Path::new("E402.py"))]
#[test_case(CheckCode::E501, Path::new("E501.py"))]
#[test_case(CheckCode::E711, Path::new("E711.py"))]
#[test_case(CheckCode::E712, Path::new("E712.py"))]
#[test_case(CheckCode::E713, Path::new("E713.py"))]
#[test_case(CheckCode::E714, Path::new("E714.py"))]
#[test_case(CheckCode::E721, Path::new("E721.py"))]
#[test_case(CheckCode::E722, Path::new("E722.py"))]
#[test_case(CheckCode::E731, Path::new("E731.py"))]
#[test_case(CheckCode::E741, Path::new("E741.py"))]
#[test_case(CheckCode::E742, Path::new("E742.py"))]
#[test_case(CheckCode::E743, Path::new("E743.py"))]
#[test_case(CheckCode::E999, Path::new("E999.py"))]
#[test_case(CheckCode::W292, Path::new("W292_0.py"))]
#[test_case(CheckCode::W292, Path::new("W292_1.py"))]
#[test_case(CheckCode::W292, Path::new("W292_2.py"))]
#[test_case(CheckCode::W605, Path::new("W605_0.py"))]
#[test_case(CheckCode::W605, Path::new("W605_1.py"))]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(

View File

@@ -0,0 +1,13 @@
---
source: src/pycodestyle/mod.rs
expression: checks
---
- kind: MultipleImportsOnOneLine
location:
row: 2
column: 0
end_location:
row: 2
column: 14
fix: ~

View File

@@ -0,0 +1,29 @@
---
source: src/pycodestyle/mod.rs
expression: checks
---
- kind: ModuleImportNotAtTopOfFile
location:
row: 55
column: 0
end_location:
row: 55
column: 10
fix: ~
- kind: ModuleImportNotAtTopOfFile
location:
row: 57
column: 0
end_location:
row: 57
column: 10
fix: ~
- kind: ModuleImportNotAtTopOfFile
location:
row: 61
column: 0
end_location:
row: 61
column: 10
fix: ~

View File

@@ -1349,39 +1349,22 @@ fn missing_args(checker: &mut Checker, docstring: &Docstring, docstrings_args: &
// See: `GOOGLE_ARGS_REGEX` in `pydocstyle/checker.py`.
static GOOGLE_ARGS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*(\w+)\s*(\(.*?\))?\s*:\n?\s*.+").unwrap());
Lazy::new(|| Regex::new(r"^\s*(\w+)\s*(\(.*?\))?\s*:.+").unwrap());
fn args_section(checker: &mut Checker, docstring: &Docstring, context: &SectionContext) {
let mut args_sections: Vec<String> = vec![];
for line in textwrap::dedent(&context.following_lines.join("\n"))
.trim()
.lines()
{
if line.chars().next().map_or(true, char::is_whitespace) {
// This is a continuation of documentation for the last
// parameter because it does start with whitespace.
if let Some(current) = args_sections.last_mut() {
current.push_str(line);
}
} else {
// This line is the start of documentation for the next
// parameter because it doesn't start with any whitespace.
args_sections.push(line.to_string());
let mut matches = Vec::new();
for line in context.following_lines {
if let Some(captures) = GOOGLE_ARGS_REGEX.captures(line) {
matches.push(captures);
}
}
missing_args(
checker,
docstring,
// Collect the list of arguments documented in the docstring.
&args_sections
&matches
.iter()
.filter_map(
|section| match GOOGLE_ARGS_REGEX.captures(section.as_str()) {
Some(caps) => caps.get(1).map(|arg_name| arg_name.as_str()),
None => None,
},
)
.filter_map(|captures| captures.get(1).map(|arg_name| arg_name.as_str()))
.collect(),
);
}

View File

@@ -69,6 +69,17 @@ expression: checks
row: 367
column: 11
fix: ~
- kind:
DocumentAllArguments:
- skip
- verbose
location:
row: 370
column: 4
end_location:
row: 382
column: 11
fix: ~
- kind:
DocumentAllArguments:
- y

View File

@@ -96,6 +96,7 @@ mod tests {
#[test_case(CheckCode::F821, Path::new("F821_4.py"); "F821_4")]
#[test_case(CheckCode::F821, Path::new("F821_5.py"); "F821_5")]
#[test_case(CheckCode::F821, Path::new("F821_6.py"); "F821_6")]
#[test_case(CheckCode::F821, Path::new("F821_7.py"); "F821_7")]
#[test_case(CheckCode::F822, Path::new("F822.py"); "F822")]
#[test_case(CheckCode::F823, Path::new("F823.py"); "F823")]
#[test_case(CheckCode::F831, Path::new("F831.py"); "F831")]

View File

@@ -0,0 +1,32 @@
---
source: src/pyflakes/mod.rs
expression: checks
---
- kind:
UndefinedName: Undefined
location:
row: 11
column: 20
end_location:
row: 11
column: 31
fix: ~
- kind:
UndefinedName: Undefined
location:
row: 12
column: 25
end_location:
row: 12
column: 36
fix: ~
- kind:
UndefinedName: Undefined
location:
row: 13
column: 20
end_location:
row: 13
column: 31
fix: ~

View File

@@ -13,7 +13,7 @@ pub fn used_prior_global_declaration(checker: &mut Checker, name: &str, expr: &E
_ => return,
};
if let Some(stmt) = globals.get(name) {
if expr.location < stmt.location {
if checker.range_for(expr).location < stmt.location {
checker.add_check(Check::new(
CheckKind::UsedPriorGlobalDeclaration(name.to_string(), stmt.location.row()),
Range::from_located(expr),

View File

@@ -134,4 +134,15 @@ expression: checks
row: 105
column: 9
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 114
location:
row: 113
column: 10
end_location:
row: 113
column: 17
fix: ~

View File

@@ -105,4 +105,18 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn datetime_utc_alias_py311() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/pyupgrade/UP017.py"),
&settings::Settings {
target_version: PythonVersion::Py311,
..settings::Settings::for_rule(CheckCode::UP017)
},
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -0,0 +1,25 @@
use rustpython_ast::Expr;
use crate::ast::helpers::{collect_call_paths, compose_call_path, dealias_call_path};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
/// UP017
pub fn datetime_utc_alias(checker: &mut Checker, expr: &Expr) {
let dealiased_call_path = dealias_call_path(collect_call_paths(expr), &checker.import_aliases);
if dealiased_call_path == ["datetime", "timezone", "utc"] {
let mut check = Check::new(CheckKind::DatetimeTimezoneUTC, Range::from_located(expr));
if checker.patch(&CheckCode::UP017) {
check.amend(Fix::replacement(
compose_call_path(expr)
.unwrap()
.replace("timezone.utc", "UTC"),
expr.location,
expr.end_location.unwrap(),
));
}
checker.add_check(check);
}
}

View File

@@ -1,5 +1,6 @@
pub use convert_named_tuple_functional_to_class::convert_named_tuple_functional_to_class;
pub use convert_typed_dict_functional_to_class::convert_typed_dict_functional_to_class;
pub use datetime_utc_alias::datetime_utc_alias;
pub use deprecated_unittest_alias::deprecated_unittest_alias;
pub use redundant_open_modes::redundant_open_modes;
pub use remove_six_compat::remove_six_compat;
@@ -15,6 +16,7 @@ pub use useless_object_inheritance::useless_object_inheritance;
mod convert_named_tuple_functional_to_class;
mod convert_typed_dict_functional_to_class;
mod datetime_utc_alias;
mod deprecated_unittest_alias;
mod redundant_open_modes;
mod remove_six_compat;

View File

@@ -0,0 +1,35 @@
---
source: src/pyupgrade/mod.rs
expression: checks
---
- kind: DatetimeTimezoneUTC
location:
row: 10
column: 6
end_location:
row: 10
column: 27
fix:
content: datetime.UTC
location:
row: 10
column: 6
end_location:
row: 10
column: 27
- kind: DatetimeTimezoneUTC
location:
row: 11
column: 6
end_location:
row: 11
column: 21
fix:
content: dt.UTC
location:
row: 11
column: 6
end_location:
row: 11
column: 21

View File

@@ -20,6 +20,7 @@ use crate::settings::{pyproject, Settings};
/// The strategy used to discover Python files in the filesystem..
#[derive(Debug)]
pub struct FileDiscovery {
pub force_exclude: bool,
pub respect_gitignore: bool,
}
@@ -163,8 +164,8 @@ pub fn resolve_settings(
/// Return `true` if the given file should be ignored based on the exclusion
/// criteria.
fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet) -> bool {
exclude.is_match(file_path) || exclude.is_match(file_basename)
fn match_exclusion(file_path: &str, file_basename: &str, exclusion: &globset::GlobSet) -> bool {
exclusion.is_match(file_path) || exclusion.is_match(file_basename)
}
/// Return `true` if the `Path` appears to be that of a Python file.
@@ -189,7 +190,7 @@ pub fn python_files_in_path(
overrides: &Overrides,
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
// Normalize every path (e.g., convert from relative to absolute).
let paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).collect();
let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).collect();
// Search for `pyproject.toml` files in all parent directories.
let mut resolver = Resolver::default();
@@ -206,6 +207,14 @@ pub fn python_files_in_path(
}
}
// Check if the paths themselves are excluded.
if file_strategy.force_exclude {
paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_strategy));
if paths.is_empty() {
return Ok((vec![], resolver));
}
}
// Create the `WalkBuilder`.
let mut builder = WalkBuilder::new(
paths
@@ -270,12 +279,16 @@ pub fn python_files_in_path(
match fs::extract_path_names(path) {
Ok((file_path, file_basename)) => {
if !settings.exclude.is_empty()
&& is_excluded(file_path, file_basename, &settings.exclude)
&& match_exclusion(file_path, file_basename, &settings.exclude)
{
debug!("Ignored path via `exclude`: {:?}", path);
return WalkState::Skip;
} else if !settings.extend_exclude.is_empty()
&& is_excluded(file_path, file_basename, &settings.extend_exclude)
&& match_exclusion(
file_path,
file_basename,
&settings.extend_exclude,
)
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
return WalkState::Skip;
@@ -302,6 +315,72 @@ pub fn python_files_in_path(
Ok((files.into_inner().unwrap(), resolver.into_inner().unwrap()))
}
/// Return `true` if the Python file at `Path` is _not_ excluded.
pub fn python_file_at_path(
path: &Path,
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<bool> {
if !file_strategy.force_exclude {
return Ok(true);
}
// Normalize the path (e.g., convert from relative to absolute).
let path = fs::normalize_path(path);
// Search for `pyproject.toml` files in all parent directories.
let mut resolver = Resolver::default();
for ancestor in path.ancestors() {
let pyproject = ancestor.join("pyproject.toml");
if pyproject.is_file() {
if has_ruff_section(&pyproject)? {
let (root, settings) =
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
resolver.add(root, settings);
}
}
}
// Check exclusions.
Ok(!is_file_excluded(&path, &resolver, pyproject_strategy))
}
/// Return `true` if the given top-level `Path` should be excluded.
fn is_file_excluded(
path: &Path,
resolver: &Resolver,
pyproject_strategy: &PyprojectDiscovery,
) -> bool {
// TODO(charlie): Respect gitignore.
for path in path.ancestors() {
if path.file_name().is_none() {
break;
}
let settings = resolver.resolve(path, pyproject_strategy);
match fs::extract_path_names(path) {
Ok((file_path, file_basename)) => {
if !settings.exclude.is_empty()
&& match_exclusion(file_path, file_basename, &settings.exclude)
{
debug!("Ignored path via `exclude`: {:?}", path);
return true;
} else if !settings.extend_exclude.is_empty()
&& match_exclusion(file_path, file_basename, &settings.extend_exclude)
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
return true;
}
}
Err(err) => {
debug!("Ignored path due to error in parsing: {:?}: {}", path, err);
return true;
}
}
}
false
}
#[cfg(test)]
mod tests {
use std::path::Path;
@@ -311,7 +390,7 @@ mod tests {
use path_absolutize::Absolutize;
use crate::fs;
use crate::resolver::{is_excluded, is_python_path};
use crate::resolver::{is_python_path, match_exclusion};
use crate::settings::types::FilePattern;
#[test]
@@ -348,7 +427,7 @@ mod tests {
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
assert!(match_exclusion(
file_path,
file_basename,
&make_exclusion(exclude,)
@@ -363,7 +442,7 @@ mod tests {
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
assert!(match_exclusion(
file_path,
file_basename,
&make_exclusion(exclude,)
@@ -380,7 +459,7 @@ mod tests {
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
assert!(match_exclusion(
file_path,
file_basename,
&make_exclusion(exclude,)
@@ -395,7 +474,7 @@ mod tests {
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
assert!(match_exclusion(
file_path,
file_basename,
&make_exclusion(exclude,)
@@ -412,7 +491,7 @@ mod tests {
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
assert!(match_exclusion(
file_path,
file_basename,
&make_exclusion(exclude,)
@@ -429,7 +508,7 @@ mod tests {
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
assert!(match_exclusion(
file_path,
file_basename,
&make_exclusion(exclude,)
@@ -446,7 +525,7 @@ mod tests {
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(!is_excluded(
assert!(!match_exclusion(
file_path,
file_basename,
&make_exclusion(exclude,)

View File

@@ -38,6 +38,7 @@ mod tests {
&settings::Settings::for_rules(vec![
CheckCode::RUF100,
CheckCode::E501,
CheckCode::F401,
CheckCode::F841,
]),
)?;

View File

@@ -182,4 +182,22 @@ expression: checks
end_location:
row: 71
column: 11
- kind:
UnusedImport:
- shelve
- false
location:
row: 85
column: 7
end_location:
row: 85
column: 13
fix:
content: ""
location:
row: 85
column: 0
end_location:
row: 86
column: 0

View File

@@ -2,11 +2,15 @@
//! command-line options. Structure mirrors the user-facing representation of
//! the various parameters.
use std::borrow::Cow;
use std::env::VarError;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use glob::{glob, GlobError, Paths, PatternError};
use regex::Regex;
use shellexpand;
use shellexpand::LookupError;
use crate::checks_gen::CheckCodePrefix;
use crate::cli::{collect_per_file_ignores, Overrides};
@@ -31,6 +35,7 @@ pub struct Configuration {
pub fix: Option<bool>,
pub fixable: Option<Vec<CheckCodePrefix>>,
pub format: Option<SerializationFormat>,
pub force_exclude: Option<bool>,
pub ignore: Option<Vec<CheckCodePrefix>>,
pub ignore_init_module_imports: Option<bool>,
pub line_length: Option<usize>,
@@ -77,7 +82,14 @@ impl Configuration {
})
.collect()
}),
extend: options.extend.map(PathBuf::from),
extend: options
.extend
.map(|extend| {
let extend = shellexpand::full(&extend);
extend.map(|extend| PathBuf::from(extend.as_ref()))
})
.transpose()
.map_err(|e| anyhow!("Invalid `extend` value: {e}"))?,
extend_exclude: options
.extend_exclude
.map(|paths| {
@@ -96,6 +108,7 @@ impl Configuration {
fix: options.fix,
fixable: options.fixable,
format: options.format,
force_exclude: options.force_exclude,
ignore: options.ignore,
ignore_init_module_imports: options.ignore_init_module_imports,
line_length: options.line_length,
@@ -159,6 +172,7 @@ impl Configuration {
fix: self.fix.or(config.fix),
fixable: self.fixable.or(config.fixable),
format: self.format.or(config.format),
force_exclude: self.force_exclude.or(config.force_exclude),
ignore: self.ignore.or(config.ignore),
ignore_init_module_imports: self
.ignore_init_module_imports
@@ -208,6 +222,9 @@ impl Configuration {
if let Some(format) = overrides.format {
self.format = Some(format);
}
if let Some(force_exclude) = overrides.force_exclude {
self.force_exclude = Some(force_exclude);
}
if let Some(ignore) = overrides.ignore {
self.ignore = Some(ignore);
}
@@ -260,9 +277,13 @@ impl Configuration {
/// Given a list of source paths, which could include glob patterns, resolve the
/// matching paths.
pub fn resolve_src(src: &[String], project_root: &Path) -> Result<Vec<PathBuf>> {
let globs = src
let expansions = src
.iter()
.map(Path::new)
.map(shellexpand::full)
.collect::<Result<Vec<Cow<'_, str>>, LookupError<VarError>>>()?;
let globs = expansions
.iter()
.map(|path| Path::new(path.as_ref()))
.map(|path| fs::normalize_path_to(path, project_root))
.map(|path| glob(&path.to_string_lossy()))
.collect::<Result<Vec<Paths>, PatternError>>()?;

View File

@@ -41,6 +41,7 @@ pub struct Settings {
pub fix: bool,
pub fixable: FxHashSet<CheckCode>,
pub format: SerializationFormat,
pub force_exclude: bool,
pub ignore_init_module_imports: bool,
pub line_length: usize,
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)>,
@@ -127,6 +128,7 @@ impl Settings {
.into_iter(),
),
format: config.format.unwrap_or(SerializationFormat::Text),
force_exclude: config.force_exclude.unwrap_or(false),
ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(),
line_length: config.line_length.unwrap_or(88),
per_file_ignores: resolve_per_file_ignores(
@@ -199,6 +201,7 @@ impl Settings {
fix: false,
fixable: FxHashSet::from_iter([check_code]),
format: SerializationFormat::Text,
force_exclude: false,
ignore_init_module_imports: false,
line_length: 88,
per_file_ignores: vec![],
@@ -231,6 +234,7 @@ impl Settings {
fix: false,
fixable: FxHashSet::from_iter(check_codes),
format: SerializationFormat::Text,
force_exclude: false,
ignore_init_module_imports: false,
line_length: 88,
per_file_ignores: vec![],
@@ -279,6 +283,7 @@ impl Hash for Settings {
}
}
self.show_source.hash(state);
self.src.hash(state);
self.target_version.hash(state);
// Add plugin properties in alphabetical order.
self.flake8_annotations.hash(state);

View File

@@ -67,7 +67,8 @@ pub struct Options {
pub exclude: Option<Vec<String>>,
#[option(
doc = r#"
A path to a local `pyproject.toml` file to merge into this configuration.
A path to a local `pyproject.toml` file to merge into this configuration. User home
directory and environment variables will be expanded.
To resolve the current `pyproject.toml` file, Ruff will first resolve this base
configuration file, then merge in any properties defined in the current configuration
@@ -165,6 +166,24 @@ pub struct Options {
"#
)]
pub format: Option<SerializationFormat>,
#[option(
doc = r#"
Whether to enforce `exclude` and `extend-exclude` patterns, even for paths that are
passed to Ruff explicitly. Typically, Ruff will lint any paths passed in directly, even
if they would typically be excluded. Setting `force-exclude = true` will cause Ruff to
respect these exclusions unequivocally.
This is useful for [`pre-commit`](https://pre-commit.com/), which explicitly passes all
changed files to the [`ruff-pre-commit`](https://github.com/charliermarsh/ruff-pre-commit)
plugin, regardless of whether they're marked as excluded by Ruff's own settings.
"#,
default = r#"false"#,
value_type = "bool",
example = r#"
force-exclude = true
"#
)]
pub force_exclude: Option<bool>,
#[option(
doc = r"
A list of check code prefixes to ignore. Prefixes can specify exact checks (like
@@ -267,7 +286,8 @@ pub struct Options {
This field supports globs. For example, if you have a series of Python packages in
a `python_modules` directory, `src = ["python_modules/*"]` would expand to incorporate
all of the packages in that directory.
all of the packages in that directory. User home directory and environment variables
will also be expanded.
"#,
default = r#"["."]"#,
value_type = "Vec<PathBuf>",

View File

@@ -129,6 +129,8 @@ mod tests {
external: None,
fix: None,
fixable: None,
format: None,
force_exclude: None,
ignore: None,
ignore_init_module_imports: None,
line_length: None,
@@ -138,7 +140,6 @@ mod tests {
show_source: None,
src: None,
target_version: None,
format: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
@@ -176,6 +177,8 @@ line-length = 79
external: None,
fix: None,
fixable: None,
force_exclude: None,
format: None,
ignore: None,
ignore_init_module_imports: None,
line_length: Some(79),
@@ -185,7 +188,6 @@ line-length = 79
show_source: None,
src: None,
target_version: None,
format: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
@@ -214,26 +216,27 @@ exclude = ["foo.py"]
Some(Tools {
ruff: Some(Options {
allowed_confusables: None,
line_length: None,
fix: None,
extend: None,
dummy_variable_rgx: None,
exclude: Some(vec!["foo.py".to_string()]),
extend: None,
extend_exclude: None,
select: None,
extend_ignore: None,
extend_select: None,
external: None,
fix: None,
fixable: None,
force_exclude: None,
format: None,
ignore: None,
ignore_init_module_imports: None,
extend_ignore: None,
fixable: None,
format: None,
unfixable: None,
line_length: None,
per_file_ignores: None,
respect_gitignore: None,
dummy_variable_rgx: None,
select: None,
show_source: None,
src: None,
target_version: None,
show_source: None,
unfixable: None,
flake8_annotations: None,
flake8_errmsg: None,
flake8_bugbear: None,
@@ -270,6 +273,8 @@ select = ["E501"]
external: None,
fix: None,
fixable: None,
force_exclude: None,
format: None,
ignore: None,
ignore_init_module_imports: None,
line_length: None,
@@ -279,7 +284,6 @@ select = ["E501"]
show_source: None,
src: None,
target_version: None,
format: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
@@ -318,6 +322,8 @@ ignore = ["E501"]
external: None,
fix: None,
fixable: None,
force_exclude: None,
format: None,
ignore: Some(vec![CheckCodePrefix::E501]),
ignore_init_module_imports: None,
line_length: None,
@@ -327,7 +333,6 @@ ignore = ["E501"]
show_source: None,
src: None,
target_version: None,
format: None,
unfixable: None,
flake8_annotations: None,
flake8_bugbear: None,
@@ -408,6 +413,7 @@ other-attribute = 1
extend_ignore: None,
fixable: None,
format: None,
force_exclude: None,
unfixable: None,
per_file_ignores: Some(FxHashMap::from_iter([(
"__init__.py".to_string(),

View File

@@ -13,7 +13,7 @@ use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::fs;
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize, Hash)]
#[serde(rename_all = "lowercase")]
pub enum PythonVersion {
Py33,

View File

@@ -23,7 +23,7 @@ fn test_stdin_error() -> Result<()> {
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"Found 1 error(s).\n-:1:8: F401 `os` imported but unused\n1 potentially fixable with the \
"-:1:8: F401 `os` imported but unused\nFound 1 error(s).\n1 potentially fixable with the \
--fix option.\n"
);
Ok(())
@@ -39,7 +39,7 @@ fn test_stdin_filename() -> Result<()> {
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"Found 1 error(s).\nF401.py:1:8: F401 `os` imported but unused\n1 potentially fixable \
"F401.py:1:8: F401 `os` imported but unused\nFound 1 error(s).\n1 potentially fixable \
with the --fix option.\n"
);
Ok(())
@@ -55,12 +55,33 @@ fn test_stdin_json() -> Result<()> {
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"[\n {\n \"code\": \"F401\",\n \"message\": \"`os` imported but unused\",\n \
\"fix\": {\n \"content\": \"\",\n \"location\": {\n \"row\": 1,\n \
\"column\": 0\n },\n \"end_location\": {\n \"row\": 2,\n \
\"column\": 0\n }\n },\n \"location\": {\n \"row\": 1,\n \
\"column\": 8\n },\n \"end_location\": {\n \"row\": 1,\n \"column\": \
10\n },\n \"filename\": \"F401.py\"\n }\n]\n"
r#"[
{
"code": "F401",
"message": "`os` imported but unused",
"fix": {
"content": "",
"location": {
"row": 1,
"column": 0
},
"end_location": {
"row": 2,
"column": 0
}
},
"location": {
"row": 1,
"column": 8
},
"end_location": {
"row": 1,
"column": 10
},
"filename": "F401.py"
}
]
"#
);
Ok(())
}