diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0df0198 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.testing.pytestArgs": [], + "python.testing.pytestEnabled": true +} diff --git a/Cargo.lock b/Cargo.lock index af34686..f33b488 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,12 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "either" version = "1.15.0" @@ -331,6 +337,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -417,6 +433,7 @@ dependencies = [ "common", "indoc", "lexical-sort", + "pretty_assertions", "pyo3", "regex", "rstest", @@ -798,6 +815,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.8.25" diff --git a/common/src/table.rs b/common/src/table.rs index 3cd4b9e..f6e67cc 100644 --- a/common/src/table.rs +++ b/common/src/table.rs @@ -1,11 +1,12 @@ use std::cell::{RefCell, RefMut}; -use std::collections::HashMap; +use std::collections::{HashMap, BTreeSet}; use std::iter::zip; use std::ops::Index; use taplo::syntax::SyntaxKind::{ENTRY, IDENT, KEY, NEWLINE, TABLE_ARRAY_HEADER, TABLE_HEADER, VALUE}; use taplo::syntax::{SyntaxElement, SyntaxNode}; use taplo::HashSet; +use taplo::{dom::node::DomNode as _, parser::parse}; use crate::create::{make_empty_newline, make_key, make_newline, make_table_entry}; use crate::string::load_text; @@ -279,7 +280,7 @@ pub fn find_key(table: &SyntaxNode, key: &str) -> Option { None } -pub fn collapse_sub_tables(tables: &mut Tables, name: &str) { +pub fn collapse_sub_tables(tables: &mut Tables, name: &str, exclude: &[Vec]) { let h2p = tables.header_to_pos.clone(); let sub_name_prefix = format!("{name}."); let sub_table_keys: Vec<&String> = h2p.keys().filter(|s| s.starts_with(sub_name_prefix.as_str())).collect(); @@ -296,6 +297,14 @@ pub fn collapse_sub_tables(tables: &mut Tables, name: &str) { if main_positions.len() != 1 { return; } + + // remove `name` from `exclude`s (and skip if `name` is not a prefix) + let prefix = parse_ident(name).expect("could not parse prefix"); + let exclude: BTreeSet<_> = exclude.into_iter().filter_map(|id| + id.strip_prefix(prefix.as_slice()) + ).collect(); + dbg!(&exclude); + let mut main = tables.table_set[*main_positions.first().unwrap()].borrow_mut(); for key in sub_table_keys { let sub_positions = tables.header_to_pos[key].clone(); @@ -304,6 +313,10 @@ pub fn collapse_sub_tables(tables: &mut Tables, name: &str) { } let mut sub = tables.table_set[*sub_positions.first().unwrap()].borrow_mut(); let sub_name = key.strip_prefix(sub_name_prefix.as_str()).unwrap(); + let sub_path = parse_ident(sub_name).unwrap(); + if exclude.contains(sub_path.as_slice()) { + continue; + } let mut header = false; for child in sub.iter() { let kind = child.kind(); @@ -340,3 +353,37 @@ pub fn collapse_sub_tables(tables: &mut Tables, name: &str) { sub.clear(); } } + +pub fn parse_ident(ident: &str) -> Result, String> { + let parsed = parse(&format!("{ident} = 1")); + if let Some(e) = parsed.errors.first() { + return Err(format!("syntax error: {e}")); + } + + let root = parsed.into_dom(); + let errors = root.errors(); + if let Some(e) = errors.get().first() { + return Err(format!("semantic error: {e}")); + } + + // We cannot use `.into_syntax()` since only the DOM transformation + // allows accessing ident `.value()`s without quotes. + let mut node = root; + let mut parts = vec![]; + while let Ok(table) = node.try_into_table() { + let entries = table.entries().get(); + if entries.len() != 1 { + return Err("expected exactly one entry".to_string()); + } + let mut it = entries.iter(); + let (key, next_node) = it.next().unwrap(); // checked if len == 1 above + + parts.push(key.value().to_string()); + node = next_node.clone(); + } + Ok(parts) +} + +fn prefixes(slice: &[T]) -> impl Iterator + DoubleEndedIterator { + (0..=slice.len()).map(|len| &slice[..len]) +} diff --git a/pyproject-fmt/Cargo.toml b/pyproject-fmt/Cargo.toml index 2a8496b..fdca5b3 100644 --- a/pyproject-fmt/Cargo.toml +++ b/pyproject-fmt/Cargo.toml @@ -25,3 +25,4 @@ default = ["extension-module"] [dev-dependencies] rstest = "0.26.1" # parametrized tests indoc = { version = "2.0.6" } # dedented test cases for literal strings +pretty_assertions = { version = "1.4.1", features = ["alloc"] } diff --git a/pyproject-fmt/rust/src/dependency_groups.rs b/pyproject-fmt/rust/src/dependency_groups.rs index 7e3c5dd..597aa77 100644 --- a/pyproject-fmt/rust/src/dependency_groups.rs +++ b/pyproject-fmt/rust/src/dependency_groups.rs @@ -8,7 +8,7 @@ use lexical_sort::natural_lexical_cmp; use std::cmp::Ordering; pub fn fix(tables: &mut Tables, keep_full_version: bool) { - collapse_sub_tables(tables, "dependency-groups"); + collapse_sub_tables(tables, "dependency-groups", &[]); let table_element = tables.get("dependency-groups"); if table_element.is_none() { return; diff --git a/pyproject-fmt/rust/src/main.rs b/pyproject-fmt/rust/src/main.rs index b827e64..7a1a77a 100644 --- a/pyproject-fmt/rust/src/main.rs +++ b/pyproject-fmt/rust/src/main.rs @@ -2,7 +2,9 @@ use std::string::String; use common::taplo::formatter::{format_syntax, Options}; use common::taplo::parser::parse; +use pyo3::exceptions::PyValueError; use pyo3::prelude::{PyModule, PyModuleMethods}; +use pyo3::types::PyTuple; use pyo3::{pyclass, pyfunction, pymethods, pymodule, wrap_pyfunction, Bound, PyResult}; use crate::global::reorder_tables; @@ -25,12 +27,13 @@ pub struct Settings { max_supported_python: (u8, u8), min_supported_python: (u8, u8), generate_python_version_classifiers: bool, + do_not_collapse: Vec>, } #[pymethods] impl Settings { #[new] - #[pyo3(signature = (*, column_width, indent, keep_full_version, max_supported_python, min_supported_python, generate_python_version_classifiers ))] + #[pyo3(signature = (*, column_width, indent, keep_full_version, max_supported_python, min_supported_python, generate_python_version_classifiers, do_not_collapse))] const fn new( column_width: usize, indent: usize, @@ -38,6 +41,7 @@ impl Settings { max_supported_python: (u8, u8), min_supported_python: (u8, u8), generate_python_version_classifiers: bool, + do_not_collapse: Vec>, ) -> Self { Self { column_width, @@ -46,6 +50,7 @@ impl Settings { max_supported_python, min_supported_python, generate_python_version_classifiers, + do_not_collapse, } } } @@ -64,9 +69,10 @@ pub fn format_toml(content: &str, opt: &Settings) -> String { opt.max_supported_python, opt.min_supported_python, opt.generate_python_version_classifiers, + opt.do_not_collapse.as_slice(), ); dependency_groups::fix(&mut tables, opt.keep_full_version); - ruff::fix(&mut tables); + ruff::fix(&mut tables, opt.do_not_collapse.as_slice()); reorder_tables(&root_ast, &tables); let options = Options { @@ -94,6 +100,16 @@ pub fn format_toml(content: &str, opt: &Settings) -> String { format_syntax(root_ast, options) } +/// Parse a nested toml identifier into a tuple of idents +/// +/// >>> parse_ident('a."b.c"') +/// ('a', 'b.c') +#[pyfunction] +pub fn parse_ident<'py>(py: pyo3::Python<'py>, ident: &str) -> PyResult> { + let parts = common::table::parse_ident(ident).map_err(|e| PyValueError::new_err(e))?; + PyTuple::new(py, parts) +} + /// # Errors /// /// Will return `PyErr` if an error is raised during formatting. @@ -101,6 +117,7 @@ pub fn format_toml(content: &str, opt: &Settings) -> String { #[pyo3(name = "_lib")] pub fn _lib(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(format_toml, m)?)?; + m.add_function(wrap_pyfunction!(parse_ident, m)?)?; m.add_class::()?; Ok(()) } diff --git a/pyproject-fmt/rust/src/project.rs b/pyproject-fmt/rust/src/project.rs index 873c4fe..1ba8709 100644 --- a/pyproject-fmt/rust/src/project.rs +++ b/pyproject-fmt/rust/src/project.rs @@ -20,8 +20,9 @@ pub fn fix( max_supported_python: (u8, u8), min_supported_python: (u8, u8), generate_python_version_classifiers: bool, + do_not_collapse: &[Vec], ) { - collapse_sub_tables(tables, "project"); + collapse_sub_tables(tables, "project", do_not_collapse); let table_element = tables.get("project"); if table_element.is_none() { return; diff --git a/pyproject-fmt/rust/src/ruff.rs b/pyproject-fmt/rust/src/ruff.rs index 8f35c8d..0d2ba83 100644 --- a/pyproject-fmt/rust/src/ruff.rs +++ b/pyproject-fmt/rust/src/ruff.rs @@ -4,8 +4,8 @@ use common::table::{collapse_sub_tables, for_entries, reorder_table_keys, Tables use lexical_sort::natural_lexical_cmp; #[allow(clippy::too_many_lines)] -pub fn fix(tables: &mut Tables) { - collapse_sub_tables(tables, "tool.ruff"); +pub fn fix(tables: &mut Tables, do_not_collapse: &[Vec]) { + collapse_sub_tables(tables, "tool.ruff", do_not_collapse); let table_element = tables.get("tool.ruff"); if table_element.is_none() { return; diff --git a/pyproject-fmt/rust/src/tests/main_tests.rs b/pyproject-fmt/rust/src/tests/main_tests.rs index e58030f..69818e0 100644 --- a/pyproject-fmt/rust/src/tests/main_tests.rs +++ b/pyproject-fmt/rust/src/tests/main_tests.rs @@ -191,6 +191,7 @@ fn test_format_toml( max_supported_python, min_supported_python: (3, 9), generate_python_version_classifiers: true, + do_not_collapse: vec![], }; let got = format_toml(start, &settings); assert_eq!(got, expected); @@ -216,6 +217,7 @@ fn test_issue_24(data: PathBuf) { max_supported_python: (3, 9), min_supported_python: (3, 9), generate_python_version_classifiers: true, + do_not_collapse: vec![], }; let got = format_toml(start.as_str(), &settings); let expected = read_to_string(data.join("ruff-order.expected.toml")).unwrap(); @@ -246,6 +248,7 @@ fn test_column_width() { max_supported_python: (3, 13), min_supported_python: (3, 13), generate_python_version_classifiers: true, + do_not_collapse: vec![], }; let got = format_toml(start, &settings); let expected = indoc! {r#" diff --git a/pyproject-fmt/rust/src/tests/project_tests.rs b/pyproject-fmt/rust/src/tests/project_tests.rs index 5dbff13..7baae38 100644 --- a/pyproject-fmt/rust/src/tests/project_tests.rs +++ b/pyproject-fmt/rust/src/tests/project_tests.rs @@ -3,6 +3,7 @@ use common::taplo::parser::parse; use common::taplo::syntax::SyntaxElement; use indoc::indoc; use rstest::rstest; +use pretty_assertions::assert_eq; use crate::project::fix; use common::table::Tables; @@ -12,6 +13,7 @@ fn evaluate( keep_full_version: bool, max_supported_python: (u8, u8), generate_python_version_classifiers: bool, + do_not_collapse: &[Vec] ) -> String { let root_ast = parse(start).into_syntax().clone_for_update(); let count = root_ast.children_with_tokens().count(); @@ -22,6 +24,7 @@ fn evaluate( max_supported_python, (3, 9), generate_python_version_classifiers, + do_not_collapse, ); let entries = tables .table_set @@ -43,6 +46,7 @@ fn evaluate( false, (3, 9), true, + &[], )] #[case::project_requires_no_keep( indoc ! {r#" @@ -63,6 +67,7 @@ fn evaluate( false, (3, 9), true, + &[], )] #[case::project_requires_keep( indoc ! {r#" @@ -83,6 +88,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_requires_ge( indoc ! {r#" @@ -121,6 +127,7 @@ fn evaluate( true, (3, 10), true, + &[], )] #[case::project_requires_gt( indoc ! {r#" @@ -138,6 +145,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_requires_eq( indoc ! {r#" @@ -155,6 +163,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_sort_keywords( indoc ! {r#" @@ -177,6 +186,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_sort_dynamic( indoc ! {r#" @@ -201,6 +211,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_name_norm( indoc ! {r#" @@ -218,6 +229,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_name_literal( indoc ! {r" @@ -235,6 +247,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_requires_gt_old( indoc ! {r#" @@ -253,6 +266,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_requires_range( indoc ! {r#" @@ -275,6 +289,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_requires_high_range( indoc ! {r#" @@ -294,6 +309,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_requires_range_neq( indoc ! {r#" @@ -312,6 +328,7 @@ fn evaluate( true, (3, 13), true, + &[], )] #[case::project_description_whitespace( "[project]\ndescription = ' A magic stuff \t is great\t\t.\r\n Like really . Works on .rst and .NET :)\t\'\nrequires-python = '==3.12'", @@ -327,6 +344,7 @@ fn evaluate( true, (3, 13), true, + &[], )] #[case::project_description_multiline( indoc ! {r#" @@ -349,6 +367,7 @@ fn evaluate( true, (3, 13), true, + &[], )] #[case::project_dependencies_with_double_quotes( indoc ! {r#" @@ -374,6 +393,7 @@ fn evaluate( true, (3, 13), true, + &[], )] #[case::project_platform_dependencies( indoc ! {r#" @@ -401,6 +421,7 @@ fn evaluate( true, (3, 13), true, + &[], )] #[case::project_opt_inline_dependencies( indoc ! {r#" @@ -432,6 +453,7 @@ fn evaluate( true, (3, 13), true, + &[], )] #[case::project_opt_dependencies( indoc ! {r#" @@ -457,6 +479,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_scripts_collapse( indoc ! {r#" @@ -476,6 +499,7 @@ fn evaluate( true, (3, 9), true, + &[], )] #[case::project_entry_points_collapse( indoc ! {r#" @@ -514,6 +538,48 @@ fn evaluate( true, (3, 9), true, + &[], +)] + +#[case::project_entry_points_no_collapse( + indoc ! {r#" + [project] + entry-points.tox = {"tox-uv" = "tox_uv.plugin", "tox" = "tox.plugin"} + [project.scripts] + virtualenv = "virtualenv.__main__:run_with_catch" + [project.gui-scripts] + hello-world = "timmins:hello_world" + [project.entry-points."virtualenv.activate"] + bash = "virtualenv.activation.bash:BashActivator" + [project.entry-points] + B = {base = "vehicle_crash_prevention.main:VehicleBase"} + [project.entry-points."no_crashes.vehicle"] + base = "vehicle_crash_prevention.main:VehicleBase" + [project.entry-points.plugin-namespace] + plugin-name1 = "pkg.subpkg1" + plugin-name2 = "pkg.subpkg2:func" + "#}, + indoc ! {r#" + [project] + classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + ] + scripts.virtualenv = "virtualenv.__main__:run_with_catch" + gui-scripts.hello-world = "timmins:hello_world" + [project.entry-points] + B.base = "vehicle_crash_prevention.main:VehicleBase" + "no_crashes.vehicle".base = "vehicle_crash_prevention.main:VehicleBase" + plugin-namespace.plugin-name1 = "pkg.subpkg1" + plugin-namespace.plugin-name2 = "pkg.subpkg2:func" + tox.tox = "tox.plugin" + tox.tox-uv = "tox_uv.plugin" + "virtualenv.activate".bash = "virtualenv.activation.bash:BashActivator" + "#}, + true, + (3, 9), + true, + &[vec!["project".to_string(), "entry-points".to_string()]], )] #[case::project_preserve_implementation_classifiers( indoc ! {r#" @@ -543,6 +609,7 @@ fn evaluate( true, (3, 10), true, + &[], )] #[case::remove_existing_python_classifiers( indoc! {r#" @@ -569,6 +636,7 @@ fn evaluate( true, (3, 10), false, + &[], )] #[case::missing_classifiers( indoc! {r#" @@ -584,6 +652,7 @@ fn evaluate( true, (3, 10), false, + &[], )] #[case::empty_classifiers( indoc! {r#" @@ -602,6 +671,7 @@ fn evaluate( true, (3, 10), false, + &[], )] #[case::preserve_non_python_classifiers( indoc! {r#" @@ -625,6 +695,7 @@ fn evaluate( true, (3, 10), false, + &[], )] fn test_format_project( #[case] start: &str, @@ -632,13 +703,15 @@ fn test_format_project( #[case] keep_full_version: bool, #[case] max_supported_python: (u8, u8), #[case] generate_python_version_classifiers: bool, + #[case] do_not_collapse: &[Vec], ) { assert_eq!( evaluate( start, keep_full_version, max_supported_python, - generate_python_version_classifiers + generate_python_version_classifiers, + do_not_collapse, ), expected ); diff --git a/pyproject-fmt/rust/src/tests/ruff_tests.rs b/pyproject-fmt/rust/src/tests/ruff_tests.rs index 48e3587..484dd67 100644 --- a/pyproject-fmt/rust/src/tests/ruff_tests.rs +++ b/pyproject-fmt/rust/src/tests/ruff_tests.rs @@ -9,11 +9,11 @@ use rstest::{fixture, rstest}; use crate::ruff::fix; use common::table::Tables; -fn evaluate(start: &str) -> String { +fn evaluate(start: &str, do_not_collapse: &[Vec]) -> String { let root_ast = parse(start).into_syntax().clone_for_update(); let count = root_ast.children_with_tokens().count(); let mut tables = Tables::from_ast(&root_ast); - fix(&mut tables); + fix(&mut tables, do_not_collapse); let entries = tables .table_set .iter() @@ -37,7 +37,7 @@ fn data() -> PathBuf { #[rstest] fn test_order_ruff(data: PathBuf) { let start = read_to_string(data.join("ruff-order.start.toml")).unwrap(); - let got = evaluate(start.as_str()); + let got = evaluate(start.as_str(), &[]); let expected = read_to_string(data.join("ruff-order.expected.toml")).unwrap(); assert_eq!(got, expected); } @@ -45,7 +45,7 @@ fn test_order_ruff(data: PathBuf) { #[rstest] fn test_ruff_comment_21(data: PathBuf) { let start = read_to_string(data.join("ruff-21.start.toml")).unwrap(); - let got = evaluate(start.as_str()); + let got = evaluate(start.as_str(), &[]); let expected = read_to_string(data.join("ruff-21.expected.toml")).unwrap(); assert_eq!(got, expected); } diff --git a/pyproject-fmt/src/pyproject_fmt/__main__.py b/pyproject-fmt/src/pyproject_fmt/__main__.py index 6a92708..e77e189 100644 --- a/pyproject-fmt/src/pyproject_fmt/__main__.py +++ b/pyproject-fmt/src/pyproject_fmt/__main__.py @@ -7,7 +7,7 @@ from toml_fmt_common import ArgumentGroup, FmtNamespace, TOMLFormatter, _build_cli, run # noqa: PLC2701 -from pyproject_fmt._lib import Settings, format_toml +from pyproject_fmt._lib import Settings, format_toml, parse_ident if TYPE_CHECKING: from collections.abc import Sequence @@ -19,6 +19,7 @@ class PyProjectFmtNamespace(FmtNamespace): keep_full_version: bool max_supported_python: tuple[int, int] generate_python_version_classifiers: bool + do_not_collapse: list[tuple[str, ...]] class PyProjectFormatter(TOMLFormatter[PyProjectFmtNamespace]): @@ -73,6 +74,15 @@ def _version_argument(got: str) -> tuple[int, int]: help="latest Python version the project supports (e.g. 3.14)", ) + parser.add_argument( + "--do-not-collapse", + metavar="table.name", + type=parse_ident, + default=[], + action="append", + help="do not collapse the given table.name (can be specified multiple times)", + ) + @property def override_cli_from_section(self) -> tuple[str, ...]: """:return: path where config overrides live""" @@ -93,6 +103,7 @@ def format(self, text: str, opt: PyProjectFmtNamespace) -> str: # noqa: PLR6301 max_supported_python=opt.max_supported_python, min_supported_python=(3, 10), # default for when the user didn't specify via requires-python generate_python_version_classifiers=opt.generate_python_version_classifiers, + do_not_collapse=opt.do_not_collapse, ) return format_toml(text, settings) diff --git a/pyproject-fmt/src/pyproject_fmt/_lib.pyi b/pyproject-fmt/src/pyproject_fmt/_lib.pyi index bebfb89..f7f04db 100644 --- a/pyproject-fmt/src/pyproject_fmt/_lib.pyi +++ b/pyproject-fmt/src/pyproject_fmt/_lib.pyi @@ -8,6 +8,7 @@ class Settings: max_supported_python: tuple[int, int], min_supported_python: tuple[int, int], generate_python_version_classifiers: bool, + do_not_collapse: list[tuple[str, ...]], ) -> None: ... @property def column_width(self) -> int: ... @@ -21,3 +22,4 @@ class Settings: def min_supported_python(self) -> tuple[int, int]: ... def format_toml(content: str, settings: Settings) -> str: ... +def parse_ident(ident: str) -> tuple[str, ...]: ... diff --git a/pyproject-fmt/tests/test_lib.py b/pyproject-fmt/tests/test_lib.py index 4d15fd9..9ea7643 100644 --- a/pyproject-fmt/tests/test_lib.py +++ b/pyproject-fmt/tests/test_lib.py @@ -4,7 +4,7 @@ import pytest -from pyproject_fmt._lib import Settings, format_toml +from pyproject_fmt._lib import Settings, format_toml, parse_ident @pytest.mark.parametrize( @@ -76,6 +76,32 @@ def test_format_toml(start: str, expected: str) -> None: min_supported_python=(3, 7), max_supported_python=(3, 8), generate_python_version_classifiers=True, + do_not_collapse=[], ) res = format_toml(dedent(start), settings) assert res == dedent(expected) + + +@pytest.mark.parametrize( + ("arg", "expected"), + [ + ("a.b", ("a", "b")), + ("a.'b.c'", ("a", "b.c")), + ], +) +def test_parse_idents(arg: str, expected: tuple[str, ...]) -> None: + assert parse_ident(arg) == expected + + +@pytest.mark.parametrize( + ("arg", "exc_cls", "msg_pat"), + [ + (None, TypeError, r"None"), + ("1 b", ValueError, r"syntax error"), + ("[]", ValueError, r"syntax error"), + ("x.", ValueError, r"syntax error"), + ], +) +def test_parse_idents_errors(arg: object, exc_cls: type[Exception], msg_pat: str) -> None: + with pytest.raises(exc_cls, match=msg_pat): + parse_ident(arg) diff --git a/pyproject.toml b/pyproject.toml index f06b6d1..c06a7b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ lint.select = [ "ALL", ] lint.ignore = [ - "ANN101", # no type annotation for self "ANN401", # allow Any as type annotation "COM812", # Conflict with formatter "CPY", # No copyright statements