Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lint pyproject.toml #4496

Merged
merged 12 commits into from
May 25, 2023
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/ruff/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ path-absolutize = { workspace = true, features = [
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.1", features = ["serde"] }
pyproject-toml = { version = "0.6.0" }
quick-junit = { version = "0.3.2" }
regex = { workspace = true }
result-like = { version = "0.4.6" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "hello-world"
version = "0.1.0"
# There's a comma missing here
dependencies = [
"tinycss2>=1.1.0<1.2",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "hello-world"
version = "0.1.0"
# Ensure that the spans from toml handle utf-8 correctly
authors = [
{ name = "Z͑ͫ̓ͪ̂ͫ̽͏̴̙̤̞͉͚̯̞̠͍A̴̵̜̰͔ͫ͗͢L̠ͨͧͩ͘G̴̻͈͍͔̹̑͗̎̅͛́Ǫ̵̹̻̝̳͂̌̌͘", email = 1 }
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# This is a valid pyproject.toml
# https://github.com/PyO3/maturin/blob/87ac3d9f74dd79ef2df9a20880b9f1fa23f9a437/pyproject.toml
[build-system]
requires = ["setuptools", "wheel>=0.36.2", "tomli>=1.1.0 ; python_version<'3.11'", "setuptools-rust>=1.4.0"]
build-backend = "setuptools.build_meta"

[project]
name = "maturin"
requires-python = ">=3.7"
classifiers = [
"Topic :: Software Development :: Build Tools",
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = ["tomli>=1.1.0 ; python_version<'3.11'"]
dynamic = [
"authors",
"description",
"license",
"readme",
"version"
]

[project.optional-dependencies]
zig = [
"ziglang~=0.10.0",
]
patchelf = [
"patchelf",
]

[project.urls]
"Source Code" = "https://github.com/PyO3/maturin"
Issues = "https://github.com/PyO3/maturin/issues"
Documentation = "https://maturin.rs"
Changelog = "https://maturin.rs/changelog.html"

[tool.maturin]
bindings = "bin"

[tool.black]
target_version = ['py37']
extend-exclude = '''
# Ignore cargo-generate templates
^/src/templates
'''

[tool.ruff]
line-length = 120
target-version = "py37"

[tool.mypy]
disallow_untyped_defs = true
disallow_incomplete_defs = true
warn_no_return = true
ignore_missing_imports = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# license-files is wrong here
# https://github.com/PyO3/maturin/issues/1615
[build-system]
requires = [ "maturin>=0.14", "numpy", "wheel", "patchelf",]
build-backend = "maturin"

[project]
name = "..."
license-files = [ "license.txt",]
requires-python = ">=3.8"
requires-dist = [ "maturin>=0.14", "...",]
dependencies = [ "packaging", "...",]
zip-safe = false
version = "..."
readme = "..."
description = "..."
classifiers = [ "...",]
[[project.authors]]
name = "..."
email = "..."

[project.urls]
homepage = "..."
documentation = "..."
repository = "..."

[project.optional-dependencies]
test = [ "coverage", "...",]
docs = [ "sphinx", "sphinx-rtd-theme",]
devel = []

[tool.maturin]
include = [ "...",]
bindings = "pyo3"
compatibility = "manylinux2014"

[tool.pytest.ini_options]
testpaths = [ "...",]
addopts = "--color=yes --tb=native --cov-report term --cov-report html:docs/dist_coverage --cov=aisdb --doctest-modules --envfile .env"
1 change: 1 addition & 0 deletions crates/ruff/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "009") => (RuleGroup::Unspecified, Rule::FunctionCallInDataclassDefaultArgument),
(Ruff, "010") => (RuleGroup::Unspecified, Rule::ExplicitFStringTypeConversion),
(Ruff, "100") => (RuleGroup::Unspecified, Rule::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, Rule::InvalidPyprojectToml),

// flake8-django
(Flake8Django, "001") => (RuleGroup::Unspecified, Rule::DjangoNullableModelStringField),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub mod logging;
pub mod message;
mod noqa;
pub mod packaging;
pub mod pyproject_toml;
pub mod registry;
pub mod resolver;
mod rule_redirects;
Expand Down
62 changes: 62 additions & 0 deletions crates/ruff/src/pyproject_toml.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use anyhow::Result;
use pyproject_toml::{BuildSystem, Project};
use ruff_text_size::{TextRange, TextSize};
use serde::{Deserialize, Serialize};

use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::SourceFile;

use crate::message::Message;
use crate::rules::ruff::rules::InvalidPyprojectToml;
use crate::IOError;

/// Unlike [`pyproject_toml::PyProjectToml`], in our case `build_system` is also optional
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
struct PyProjectToml {
/// Build-related data
build_system: Option<BuildSystem>,
/// Project metadata
project: Option<Project>,
}

pub fn lint_pyproject_toml(source_file: SourceFile) -> Result<Vec<Message>> {
let err = match toml::from_str::<PyProjectToml>(source_file.source_text()) {
Ok(_) => return Ok(Vec::default()),
Err(err) => err,
};

let range = match err.span() {
// This is bad but sometimes toml and/or serde just don't give us spans
// TODO(konstin,micha): https://github.com/charliermarsh/ruff/issues/4571
None => TextRange::default(),
Some(range) => {
let Ok(end) = TextSize::try_from(range.end) else {
let diagnostic = Diagnostic::new(
IOError {
message: "pyproject.toml is larger than 4GB".to_string(),
},
TextRange::default(),
);
return Ok(vec![Message::from_diagnostic(
diagnostic,
source_file,
TextSize::default(),
)]);
};
TextRange::new(
// start <= end, so if end < 4GB follows start < 4GB
TextSize::try_from(range.start).unwrap(),
end,
)
}
};

let toml_err = err.message().to_string();
let diagnostic = Diagnostic::new(InvalidPyprojectToml { message: toml_err }, range);
Ok(vec![Message::from_diagnostic(
diagnostic,
source_file,
TextSize::default(),
)])
}
1 change: 1 addition & 0 deletions crates/ruff/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@ ruff_macros::register_rules!(
rules::ruff::rules::MutableDataclassDefault,
rules::ruff::rules::FunctionCallInDataclassDefaultArgument,
rules::ruff::rules::ExplicitFStringTypeConversion,
rules::ruff::rules::InvalidPyprojectToml,
// flake8-django
rules::flake8_django::rules::DjangoNullableModelStringField,
rules::flake8_django::rules::DjangoLocalsInRenderFunction,
Expand Down
24 changes: 23 additions & 1 deletion crates/ruff/src/rules/ruff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ pub(crate) mod rules;

#[cfg(test)]
mod tests {
use std::fs;
use std::path::Path;

use anyhow::Result;
use rustc_hash::FxHashSet;
use test_case::test_case;

use ruff_python_ast::source_code::SourceFileBuilder;

use crate::pyproject_toml::lint_pyproject_toml;
use crate::registry::Rule;
use crate::settings::resolve_per_file_ignores;
use crate::settings::types::PerFileIgnore;
use crate::test::test_path;
use crate::test::{test_path, test_resource_path};
use crate::{assert_messages, settings};

#[test_case(Rule::ExplicitFStringTypeConversion, Path::new("RUF010.py"); "RUF010")]
Expand Down Expand Up @@ -174,4 +178,22 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}

#[test_case(Rule::InvalidPyprojectToml, Path::new("bleach"))]
#[test_case(Rule::InvalidPyprojectToml, Path::new("invalid_author"))]
#[test_case(Rule::InvalidPyprojectToml, Path::new("maturin"))]
#[test_case(Rule::InvalidPyprojectToml, Path::new("maturin_gh_1615"))]
fn invalid_pyproject_toml(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let path = test_resource_path("fixtures")
.join("ruff")
.join("pyproject_toml")
.join(path)
.join("pyproject.toml");
let contents = fs::read_to_string(path)?;
let source_file = SourceFileBuilder::new("pyproject.toml", contents).finish();
let messages = lint_pyproject_toml(source_file)?;
assert_messages!(snapshot, messages);
Ok(())
}
}
45 changes: 45 additions & 0 deletions crates/ruff/src/rules/ruff/rules/invalid_pyproject_toml.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use ruff_diagnostics::{AutofixKind, Violation};
use ruff_macros::{derive_message_formats, violation};

/// ## What it does
/// Checks for any pyproject.toml that does not conform to the schema from the relevant PEPs.
///
/// ## Why is this bad?
/// Your project may contain invalid metadata or configuration without you noticing
///
/// ## Example
/// ```toml
/// [project]
/// name = "crab"
/// version = "1.0.0"
/// authors = ["Ferris the Crab <ferris@example.org>"]
/// ```
///
/// Use instead:
/// ```toml
/// [project]
/// name = "crab"
/// version = "1.0.0"
/// authors = [
/// { email = "ferris@example.org" },
/// { name = "Ferris the Crab"}
/// ]
/// ```
///
/// ## References
/// - [Specification of `[project]` in pyproject.toml](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/)
/// - [Specification of `[build-system]` in pyproject.toml](https://peps.python.org/pep-0518/)
/// - [Draft but implemented license declaration extensions](https://peps.python.org/pep-0639)
#[violation]
pub struct InvalidPyprojectToml {
pub message: String,
}

impl Violation for InvalidPyprojectToml {
const AUTOFIX: AutofixKind = AutofixKind::None;

#[derive_message_formats]
fn message(&self) -> String {
format!("Failed to parse pyproject.toml: {}", self.message)
}
}
2 changes: 2 additions & 0 deletions crates/ruff/src/rules/ruff/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub(crate) use collection_literal_concatenation::{
pub(crate) use explicit_f_string_type_conversion::{
explicit_f_string_type_conversion, ExplicitFStringTypeConversion,
};
pub(crate) use invalid_pyproject_toml::InvalidPyprojectToml;
pub(crate) use mutable_defaults_in_dataclass_fields::{
function_call_in_dataclass_defaults, is_dataclass, mutable_dataclass_default,
FunctionCallInDataclassDefaultArgument, MutableDataclassDefault,
Expand All @@ -21,6 +22,7 @@ mod asyncio_dangling_task;
mod collection_literal_concatenation;
mod confusables;
mod explicit_f_string_type_conversion;
mod invalid_pyproject_toml;
mod mutable_defaults_in_dataclass_fields;
mod pairwise_over_zipped;
mod unused_noqa;
Expand Down