diff --git a/Cargo.lock b/Cargo.lock index 4f03ba3d6b..9c454bb52e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3192,6 +3192,7 @@ dependencies = [ "thiserror 2.0.12", "toml", "tracing", + "tracing-log", "tracing-subscriber", ] diff --git a/Cargo.toml b/Cargo.toml index f3782c5f14..cebdae212b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,6 +154,7 @@ toml = { version = "0.8.11" } tracing = { version = "0.1.40" } tracing-flame = { version = "0.2.0" } tracing-indicatif = { version = "0.3.6" } +tracing-log = { version = "0.2.0" } tracing-subscriber = { version = "0.3.18", default-features = false, features = [ "env-filter", "fmt", diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index 8469c577d9..882240b3b6 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -153,7 +153,11 @@ pub fn run( })); } - set_up_logging(global_options.log_level())?; + // Don't set up logging for the server command, as it has its own logging setup + // and setting the global logger can only be done once. + if !matches!(command, Command::Server { .. }) { + set_up_logging(global_options.log_level())?; + } match command { Command::Version { output_format } => { diff --git a/crates/ruff/src/resolve.rs b/crates/ruff/src/resolve.rs index 89796ee156..4fbea4dad6 100644 --- a/crates/ruff/src/resolve.rs +++ b/crates/ruff/src/resolve.rs @@ -5,12 +5,14 @@ use log::debug; use path_absolutize::path_dedot; use ruff_workspace::configuration::Configuration; -use ruff_workspace::pyproject; +use ruff_workspace::pyproject::{self, find_fallback_target_version}; use ruff_workspace::resolver::{ - resolve_root_settings, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, - Relativity, + resolve_root_settings, ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig, + PyprojectDiscoveryStrategy, }; +use ruff_python_ast as ast; + use crate::args::ConfigArguments; /// Resolve the relevant settings strategy and defaults for the current @@ -35,7 +37,11 @@ pub fn resolve( // `pyproject.toml` for _all_ configuration, and resolve paths relative to the // current working directory. (This matches ESLint's behavior.) if let Some(pyproject) = config_arguments.config_file() { - let settings = resolve_root_settings(pyproject, Relativity::Cwd, config_arguments)?; + let settings = resolve_root_settings( + pyproject, + config_arguments, + ConfigurationOrigin::UserSpecified, + )?; debug!( "Using user-specified configuration file at: {}", pyproject.display() @@ -61,7 +67,8 @@ pub fn resolve( "Using configuration file (via parent) at: {}", pyproject.display() ); - let settings = resolve_root_settings(&pyproject, Relativity::Parent, config_arguments)?; + let settings = + resolve_root_settings(&pyproject, config_arguments, ConfigurationOrigin::Ancestor)?; return Ok(PyprojectConfig::new( PyprojectDiscoveryStrategy::Hierarchical, settings, @@ -74,11 +81,35 @@ pub fn resolve( // end up the "closest" `pyproject.toml` file for every Python file later on, so // these act as the "default" settings.) if let Some(pyproject) = pyproject::find_user_settings_toml() { + struct FallbackTransformer<'a> { + arguments: &'a ConfigArguments, + } + + impl ConfigurationTransformer for FallbackTransformer<'_> { + fn transform(&self, mut configuration: Configuration) -> Configuration { + // The `requires-python` constraint from the `pyproject.toml` takes precedence + // over the `target-version` from the user configuration. + let fallback = find_fallback_target_version(&*path_dedot::CWD); + if let Some(fallback) = fallback { + debug!("Derived `target-version` from found `requires-python`: {fallback:?}"); + configuration.target_version = Some(fallback.into()); + } + + self.arguments.transform(configuration) + } + } + debug!( "Using configuration file (via cwd) at: {}", pyproject.display() ); - let settings = resolve_root_settings(&pyproject, Relativity::Cwd, config_arguments)?; + let settings = resolve_root_settings( + &pyproject, + &FallbackTransformer { + arguments: config_arguments, + }, + ConfigurationOrigin::UserSettings, + )?; return Ok(PyprojectConfig::new( PyprojectDiscoveryStrategy::Hierarchical, settings, @@ -91,7 +122,24 @@ pub fn resolve( // "closest" `pyproject.toml` file for every Python file later on, so these act // as the "default" settings.) debug!("Using Ruff default settings"); - let config = config_arguments.transform(Configuration::default()); + let mut config = config_arguments.transform(Configuration::default()); + if config.target_version.is_none() { + // If we have arrived here we know that there was no `pyproject.toml` + // containing a `[tool.ruff]` section found in an ancestral directory. + // (This is an implicit requirement in the function + // `pyproject::find_settings_toml`.) + // However, there may be a `pyproject.toml` with a `requires-python` + // specified, and that is what we look for in this step. + let fallback = find_fallback_target_version( + stdin_filename + .as_ref() + .unwrap_or(&path_dedot::CWD.as_path()), + ); + if let Some(version) = fallback { + debug!("Derived `target-version` from found `requires-python`: {version:?}"); + } + config.target_version = fallback.map(ast::PythonVersion::from); + } let settings = config.into_settings(&path_dedot::CWD)?; Ok(PyprojectConfig::new( PyprojectDiscoveryStrategy::Hierarchical, diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index 702723d54b..1fdead18d5 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -2085,6 +2085,2637 @@ select = ["UP006"] Ok(()) } +/// ``` +/// tmp +/// ├── pyproject.toml #<--- no `[tool.ruff]` +/// └── test.py +/// ``` +#[test] +fn requires_python_no_tool() -> Result<()> { + let tempdir = TempDir::new()?; + let project_dir = tempdir.path().canonicalize()?; + let ruff_toml = tempdir.path().join("pyproject.toml"); + fs::write( + &ruff_toml, + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + let testpy = tempdir.path().join("test.py"); + fs::write( + &testpy, + r#"from typing import Union;foo: Union[int, str] = 1"#, + )?; + insta::with_settings!({ + filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--show-settings") + .args(["--select","UP007"]) + .arg("test.py") + .arg("-") + .current_dir(project_dir) + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/test.py" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.11 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.11 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.11 + analyze.detect_string_imports = false + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + }); + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml #<--- no `[tool.ruff]` +/// └── test.py +/// ``` +#[test] +fn requires_python_no_tool_target_version_override() -> Result<()> { + let tempdir = TempDir::new()?; + let project_dir = tempdir.path().canonicalize()?; + let ruff_toml = tempdir.path().join("pyproject.toml"); + fs::write( + &ruff_toml, + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + let testpy = tempdir.path().join("test.py"); + fs::write( + &testpy, + r#"from typing import Union;foo: Union[int, str] = 1"#, + )?; + insta::with_settings!({ + filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--show-settings") + .args(["--select","UP007"]) + .args(["--target-version","py310"]) + .arg("test.py") + .arg("-") + .current_dir(project_dir) + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/test.py" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.10 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.10 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.10 + analyze.detect_string_imports = false + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + }); + Ok(()) +} +/// ``` +/// tmp +/// ├── pyproject.toml #<--- no `[tool.ruff]` +/// └── test.py +/// ``` +#[test] +fn requires_python_no_tool_with_check() -> Result<()> { + let tempdir = TempDir::new()?; + let project_dir = tempdir.path().canonicalize()?; + let ruff_toml = tempdir.path().join("pyproject.toml"); + fs::write( + &ruff_toml, + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + let testpy = tempdir.path().join("test.py"); + fs::write( + &testpy, + r#"from typing import Union;foo: Union[int, str] = 1"#, + )?; + insta::with_settings!({ + filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .args(["--select","UP007"]) + .arg(".") + .current_dir(project_dir) + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + test.py:1:31: UP007 [*] Use `X | Y` for type annotations + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "###); + }); + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml #<-- no [tool.ruff] +/// ├── ruff.toml #<-- no `target-version` +/// └── test.py +/// ``` +#[test] +fn requires_python_ruff_toml_no_target_fallback() -> Result<()> { + let tempdir = TempDir::new()?; + let project_dir = tempdir.path().canonicalize()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#"[lint] +select = ["UP007"] +"#, + )?; + + let pyproject_toml = tempdir.path().join("pyproject.toml"); + fs::write( + &pyproject_toml, + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + let testpy = tempdir.path().join("test.py"); + fs::write( + &testpy, + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("test.py") + .arg("--show-settings") + .current_dir(project_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/test.py" + Settings path: "[TMP]/ruff.toml" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.11 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.11 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.11 + analyze.detect_string_imports = false + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + }); + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml #<-- no [tool.ruff] +/// ├── ruff.toml #<-- no `target-version` +/// └── test.py +/// ``` +#[test] +fn requires_python_ruff_toml_no_target_fallback_check() -> Result<()> { + let tempdir = TempDir::new()?; + let project_dir = tempdir.path().canonicalize()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#"[lint] +select = ["UP007"] +"#, + )?; + + let pyproject_toml = tempdir.path().join("pyproject.toml"); + fs::write( + &pyproject_toml, + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + let testpy = tempdir.path().join("test.py"); + fs::write( + &testpy, + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&project_dir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg(".") + .current_dir(project_dir), @r###" + success: false + exit_code: 1 + ----- stdout ----- + test.py:2:31: UP007 [*] Use `X | Y` for type annotations + Found 1 error. + [*] 1 fixable with the `--fix` option. + + ----- stderr ----- + "###); + }); + Ok(()) +} + +/// ``` +/// tmp +/// ├── foo +/// │ ├── pyproject.toml #<-- no [tool.ruff], no `requires-python` +/// │ └── test.py +/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python` +/// ``` +#[test] +fn requires_python_pyproject_toml_above() -> Result<()> { + let tempdir = TempDir::new()?; + let project_dir = tempdir.path().canonicalize()?; + let outer_pyproject = tempdir.path().join("pyproject.toml"); + fs::write( + &outer_pyproject, + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + let foodir = tempdir.path().join("foo"); + fs::create_dir(foodir)?; + + let inner_pyproject = tempdir.path().join("foo/pyproject.toml"); + fs::write( + &inner_pyproject, + r#"[project] +"#, + )?; + + let testpy = tempdir.path().join("foo/test.py"); + fs::write( + &testpy, + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + let testpy_canon = testpy.canonicalize()?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/"),(r"(?m)^foo\\test","foo/test")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--show-settings") + .args(["--select","UP007"]) + .arg("foo/test.py") + .current_dir(&project_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/foo/test.py" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.11 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.11 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.11 + analyze.detect_string_imports = false + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + }); + Ok(()) +} + +/// ``` +/// tmp +/// ├── foo +/// │ ├── pyproject.toml #<-- has [tool.ruff], no `requires-python` +/// │ └── test.py +/// └── pyproject.toml #<-- no [tool.ruff], has `requires-python` +/// ``` +#[test] +fn requires_python_pyproject_toml_above_with_tool() -> Result<()> { + let tempdir = TempDir::new()?; + let project_dir = tempdir.path().canonicalize()?; + let outer_pyproject = tempdir.path().join("pyproject.toml"); + fs::write( + &outer_pyproject, + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + let foodir = tempdir.path().join("foo"); + fs::create_dir(foodir)?; + + let inner_pyproject = tempdir.path().join("foo/pyproject.toml"); + fs::write( + &inner_pyproject, + r#" +[tool.ruff] +target-version = "py310" +"#, + )?; + + let testpy = tempdir.path().join("foo/test.py"); + fs::write( + &testpy, + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + let testpy_canon = testpy.canonicalize()?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/"),(r"foo\\","foo/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--show-settings") + .args(["--select","UP007"]) + .arg("foo/test.py") + .current_dir(&project_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/foo/test.py" + + # General Settings + cache_dir = "[TMP]/foo/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/foo" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/foo" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.10 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/foo", + "[TMP]/foo/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.10 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.10 + analyze.detect_string_imports = false + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + }); + Ok(()) +} + +/// ``` +/// tmp +/// ├── foo +/// │ ├── pyproject.toml #<-- no [tool.ruff] +/// │ └── test.py +/// └── ruff.toml #<-- no `target-version` +/// ``` +#[test] +fn requires_python_ruff_toml_above() -> Result<()> { + let tempdir = TempDir::new()?; + let project_dir = tempdir.path().canonicalize()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +[lint] +select = ["UP007"] +"#, + )?; + + let foodir = tempdir.path().join("foo"); + fs::create_dir(foodir)?; + + let pyproject_toml = tempdir.path().join("foo/pyproject.toml"); + fs::write( + &pyproject_toml, + r#"[project] +requires-python = ">= 3.11" +"#, + )?; + + let testpy = tempdir.path().join("foo/test.py"); + fs::write( + &testpy, + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + let testpy_canon = testpy.canonicalize()?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--show-settings") + .arg("foo/test.py") + .current_dir(&project_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/foo/test.py" + Settings path: "[TMP]/ruff.toml" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.9 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.9 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.9 + analyze.detect_string_imports = false + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + }); + + insta::with_settings!({ + filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/foo/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--show-settings") + .arg("test.py") + .current_dir(project_dir.join("foo")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/foo/test.py" + Settings path: "[TMP]/ruff.toml" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.9 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.9 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.9 + analyze.detect_string_imports = false + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + }); + Ok(()) +} + +/// ``` +/// tmp +/// ├── pyproject.toml <-- requires >=3.10 +/// ├── ruff.toml <--- extends base +/// ├── shared +/// │ └── base_config.toml <-- targets 3.11 +/// └── test.py +/// ``` +#[test] +fn requires_python_extend_from_shared_config() -> Result<()> { + let tempdir = TempDir::new()?; + let project_dir = tempdir.path().canonicalize()?; + let ruff_toml = tempdir.path().join("ruff.toml"); + fs::write( + &ruff_toml, + r#" +extend = "./shared/base_config.toml" +[lint] +select = ["UP007"] +"#, + )?; + + let shared_dir = tempdir.path().join("shared"); + fs::create_dir(shared_dir)?; + + let pyproject_toml = tempdir.path().join("pyproject.toml"); + fs::write( + &pyproject_toml, + r#"[project] +requires-python = ">= 3.10" +"#, + )?; + + let shared_toml = tempdir.path().join("shared/base_config.toml"); + fs::write( + &shared_toml, + r#" +target-version = "py311" +"#, + )?; + + let testpy = tempdir.path().join("test.py"); + fs::write( + &testpy, + r#" +from typing import Union;foo: Union[int, str] = 1 +"#, + )?; + + let testpy_canon = testpy.canonicalize()?; + + insta::with_settings!({ + filters => vec![(tempdir_filter(&testpy_canon).as_str(), "[TMP]/test.py"),(tempdir_filter(&project_dir).as_str(), "[TMP]/")] + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(STDIN_BASE_OPTIONS) + .arg("--show-settings") + .arg("test.py") + .current_dir(&project_dir), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Resolved settings for: "[TMP]/test.py" + Settings path: "[TMP]/ruff.toml" + + # General Settings + cache_dir = "[TMP]/.ruff_cache" + fix = false + fix_only = false + output_format = concise + show_fixes = false + unsafe_fixes = hint + + # File Resolver Settings + file_resolver.exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "dist", + "node_modules", + "site-packages", + "venv", + ] + file_resolver.extend_exclude = [] + file_resolver.force_exclude = false + file_resolver.include = [ + "*.py", + "*.pyi", + "*.ipynb", + "**/pyproject.toml", + ] + file_resolver.extend_include = [] + file_resolver.respect_gitignore = true + file_resolver.project_root = "[TMP]/" + + # Linter Settings + linter.exclude = [] + linter.project_root = "[TMP]/" + linter.rules.enabled = [ + non-pep604-annotation-union (UP007), + ] + linter.rules.should_fix = [ + non-pep604-annotation-union (UP007), + ] + linter.per_file_ignores = {} + linter.safety_table.forced_safe = [] + linter.safety_table.forced_unsafe = [] + linter.unresolved_target_version = 3.10 + linter.per_file_target_version = {} + linter.preview = disabled + linter.explicit_preview_rules = false + linter.extension = ExtensionMapping({}) + linter.allowed_confusables = [] + linter.builtins = [] + linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$ + linter.external = [] + linter.ignore_init_module_imports = true + linter.logger_objects = [] + linter.namespace_packages = [] + linter.src = [ + "[TMP]/", + "[TMP]/src", + ] + linter.tab_size = 4 + linter.line_length = 88 + linter.task_tags = [ + TODO, + FIXME, + XXX, + ] + linter.typing_modules = [] + + # Linter Plugins + linter.flake8_annotations.mypy_init_return = false + linter.flake8_annotations.suppress_dummy_args = false + linter.flake8_annotations.suppress_none_returning = false + linter.flake8_annotations.allow_star_arg_any = false + linter.flake8_annotations.ignore_fully_untyped = false + linter.flake8_bandit.hardcoded_tmp_directory = [ + /tmp, + /var/tmp, + /dev/shm, + ] + linter.flake8_bandit.check_typed_exception = false + linter.flake8_bandit.extend_markup_names = [] + linter.flake8_bandit.allowed_markup_calls = [] + linter.flake8_bugbear.extend_immutable_calls = [] + linter.flake8_builtins.allowed_modules = [] + linter.flake8_builtins.ignorelist = [] + linter.flake8_builtins.strict_checking = false + linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false + linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})* + linter.flake8_copyright.author = none + linter.flake8_copyright.min_file_size = 0 + linter.flake8_errmsg.max_string_length = 0 + linter.flake8_gettext.functions_names = [ + _, + gettext, + ngettext, + ] + linter.flake8_implicit_str_concat.allow_multiline = true + linter.flake8_import_conventions.aliases = { + altair = alt, + holoviews = hv, + matplotlib = mpl, + matplotlib.pyplot = plt, + networkx = nx, + numpy = np, + pandas = pd, + panel = pn, + plotly.express = px, + polars = pl, + pyarrow = pa, + seaborn = sns, + tensorflow = tf, + tkinter = tk, + xml.etree.ElementTree = ET, + } + linter.flake8_import_conventions.banned_aliases = {} + linter.flake8_import_conventions.banned_from = [] + linter.flake8_pytest_style.fixture_parentheses = false + linter.flake8_pytest_style.parametrize_names_type = tuple + linter.flake8_pytest_style.parametrize_values_type = list + linter.flake8_pytest_style.parametrize_values_row_type = tuple + linter.flake8_pytest_style.raises_require_match_for = [ + BaseException, + Exception, + ValueError, + OSError, + IOError, + EnvironmentError, + socket.error, + ] + linter.flake8_pytest_style.raises_extend_require_match_for = [] + linter.flake8_pytest_style.mark_parentheses = false + linter.flake8_quotes.inline_quotes = double + linter.flake8_quotes.multiline_quotes = double + linter.flake8_quotes.docstring_quotes = double + linter.flake8_quotes.avoid_escape = true + linter.flake8_self.ignore_names = [ + _make, + _asdict, + _replace, + _fields, + _field_defaults, + _name_, + _value_, + ] + linter.flake8_tidy_imports.ban_relative_imports = "parents" + linter.flake8_tidy_imports.banned_api = {} + linter.flake8_tidy_imports.banned_module_level_imports = [] + linter.flake8_type_checking.strict = false + linter.flake8_type_checking.exempt_modules = [ + typing, + typing_extensions, + ] + linter.flake8_type_checking.runtime_required_base_classes = [] + linter.flake8_type_checking.runtime_required_decorators = [] + linter.flake8_type_checking.quote_annotations = false + linter.flake8_unused_arguments.ignore_variadic_names = false + linter.isort.required_imports = [] + linter.isort.combine_as_imports = false + linter.isort.force_single_line = false + linter.isort.force_sort_within_sections = false + linter.isort.detect_same_package = true + linter.isort.case_sensitive = false + linter.isort.force_wrap_aliases = false + linter.isort.force_to_top = [] + linter.isort.known_modules = {} + linter.isort.order_by_type = true + linter.isort.relative_imports_order = furthest_to_closest + linter.isort.single_line_exclusions = [] + linter.isort.split_on_trailing_comma = true + linter.isort.classes = [] + linter.isort.constants = [] + linter.isort.variables = [] + linter.isort.no_lines_before = [] + linter.isort.lines_after_imports = -1 + linter.isort.lines_between_types = 0 + linter.isort.forced_separate = [] + linter.isort.section_order = [ + known { type = future }, + known { type = standard_library }, + known { type = third_party }, + known { type = first_party }, + known { type = local_folder }, + ] + linter.isort.default_section = known { type = third_party } + linter.isort.no_sections = false + linter.isort.from_first = false + linter.isort.length_sort = false + linter.isort.length_sort_straight = false + linter.mccabe.max_complexity = 10 + linter.pep8_naming.ignore_names = [ + setUp, + tearDown, + setUpClass, + tearDownClass, + setUpModule, + tearDownModule, + asyncSetUp, + asyncTearDown, + setUpTestData, + failureException, + longMessage, + maxDiff, + ] + linter.pep8_naming.classmethod_decorators = [] + linter.pep8_naming.staticmethod_decorators = [] + linter.pycodestyle.max_line_length = 88 + linter.pycodestyle.max_doc_length = none + linter.pycodestyle.ignore_overlong_task_comments = false + linter.pyflakes.extend_generics = [] + linter.pyflakes.allowed_unused_imports = [] + linter.pylint.allow_magic_value_types = [ + str, + bytes, + ] + linter.pylint.allow_dunder_method_names = [] + linter.pylint.max_args = 5 + linter.pylint.max_positional_args = 5 + linter.pylint.max_returns = 6 + linter.pylint.max_bool_expr = 5 + linter.pylint.max_branches = 12 + linter.pylint.max_statements = 50 + linter.pylint.max_public_methods = 20 + linter.pylint.max_locals = 15 + linter.pyupgrade.keep_runtime_typing = false + linter.ruff.parenthesize_tuple_in_subscript = false + + # Formatter Settings + formatter.exclude = [] + formatter.unresolved_target_version = 3.10 + formatter.per_file_target_version = {} + formatter.preview = disabled + formatter.line_width = 88 + formatter.line_ending = auto + formatter.indent_style = space + formatter.indent_width = 4 + formatter.quote_style = double + formatter.magic_trailing_comma = respect + formatter.docstring_code_format = disabled + formatter.docstring_code_line_width = dynamic + + # Analyze Settings + analyze.exclude = [] + analyze.preview = disabled + analyze.target_version = 3.10 + analyze.detect_string_imports = false + analyze.extension = ExtensionMapping({}) + analyze.include_dependencies = {} + + ----- stderr ----- + "###); + }); + + Ok(()) +} + #[test] fn checks_notebooks_in_stable() -> anyhow::Result<()> { let tempdir = TempDir::new()?; @@ -2427,23 +5058,23 @@ class Foo[_T, __T]: /// construct a directory tree with this structure: /// . /// ├── abc -/// │   └── __init__.py +/// │ └── __init__.py /// ├── collections -/// │   ├── __init__.py -/// │   ├── abc -/// │   │   └── __init__.py -/// │   └── foobar -/// │   └── __init__.py +/// │ ├── __init__.py +/// │ ├── abc +/// │ │ └── __init__.py +/// │ └── foobar +/// │ └── __init__.py /// ├── foobar -/// │   ├── __init__.py -/// │   ├── abc -/// │   │   └── __init__.py -/// │   └── collections -/// │   ├── __init__.py -/// │   ├── abc -/// │   │   └── __init__.py -/// │   └── foobar -/// │   └── __init__.py +/// │ ├── __init__.py +/// │ ├── abc +/// │ │ └── __init__.py +/// │ └── collections +/// │ ├── __init__.py +/// │ ├── abc +/// │ │ └── __init__.py +/// │ └── foobar +/// │ └── __init__.py /// ├── ruff.toml /// └── urlparse /// └── __init__.py diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index a3d0b7cee5..5b7e682d07 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -40,6 +40,7 @@ shellexpand = { workspace = true } thiserror = { workspace = true } toml = { workspace = true } tracing = { workspace = true } +tracing-log = { workspace = true } tracing-subscriber = { workspace = true, features = ["chrono"] } [dev-dependencies] diff --git a/crates/ruff_server/src/logging.rs b/crates/ruff_server/src/logging.rs index d4747cadae..c402c3a869 100644 --- a/crates/ruff_server/src/logging.rs +++ b/crates/ruff_server/src/logging.rs @@ -64,6 +64,8 @@ pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Pat tracing::subscriber::set_global_default(subscriber) .expect("should be able to set global default subscriber"); + + tracing_log::LogTracer::init().unwrap(); } /// The log level for the server as provided by the client during initialization. diff --git a/crates/ruff_server/src/session/index/ruff_settings.rs b/crates/ruff_server/src/session/index/ruff_settings.rs index b7c93dde07..c004f1d9f6 100644 --- a/crates/ruff_server/src/session/index/ruff_settings.rs +++ b/crates/ruff_server/src/session/index/ruff_settings.rs @@ -9,12 +9,13 @@ use ignore::{WalkBuilder, WalkState}; use ruff_linter::settings::types::GlobPath; use ruff_linter::{settings::types::FilePattern, settings::types::PreviewMode}; +use ruff_workspace::pyproject::find_fallback_target_version; use ruff_workspace::resolver::match_exclusion; use ruff_workspace::Settings; use ruff_workspace::{ configuration::{Configuration, FormatConfiguration, LintConfiguration, RuleSelection}, pyproject::{find_user_settings_toml, settings_toml}, - resolver::{ConfigurationTransformer, Relativity}, + resolver::ConfigurationTransformer, }; use crate::session::settings::{ @@ -64,12 +65,36 @@ impl RuffSettings { /// In the absence of a valid configuration file, it gracefully falls back to /// editor-only settings. pub(crate) fn fallback(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings { + struct FallbackTransformer<'a> { + inner: EditorConfigurationTransformer<'a>, + } + + impl ConfigurationTransformer for FallbackTransformer<'_> { + fn transform(&self, mut configuration: Configuration) -> Configuration { + let fallback = find_fallback_target_version(self.inner.1); + if let Some(fallback) = fallback { + tracing::debug!( + "Derived `target-version` from found `requires-python`: {fallback:?}" + ); + configuration.target_version = Some(fallback.into()); + } + + self.inner.transform(configuration) + } + } + find_user_settings_toml() .and_then(|user_settings| { + tracing::debug!( + "Loading settings from user configuration file: `{}`", + user_settings.display() + ); ruff_workspace::resolver::resolve_root_settings( &user_settings, - Relativity::Cwd, - &EditorConfigurationTransformer(editor_settings, root), + &FallbackTransformer { + inner: EditorConfigurationTransformer(editor_settings, root), + }, + ruff_workspace::resolver::ConfigurationOrigin::UserSettings, ) .ok() .map(|settings| RuffSettings { @@ -77,21 +102,45 @@ impl RuffSettings { settings, }) }) - .unwrap_or_else(|| Self::editor_only(editor_settings, root)) + .unwrap_or_else(|| { + let fallback = find_fallback_target_version(root); + if let Some(fallback) = fallback { + tracing::debug!( + "Derived `target-version` from found `requires-python` for fallback configuration: {fallback:?}" + ); + } + + let configuration = Configuration { + target_version: fallback.map(Into::into), + ..Configuration::default() + }; + Self::with_editor_settings(editor_settings, root, configuration).expect( + "editor configuration should merge successfully with default configuration", + ) + }) } /// Constructs [`RuffSettings`] by merging the editor-defined settings with the /// default configuration. fn editor_only(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings { - let settings = EditorConfigurationTransformer(editor_settings, root) - .transform(Configuration::default()) - .into_settings(root) - .expect("editor configuration should merge successfully with default configuration"); + Self::with_editor_settings(editor_settings, root, Configuration::default()) + .expect("editor configuration should merge successfully with default configuration") + } - RuffSettings { + /// Merges the `configuration` with the editor defined settings. + fn with_editor_settings( + editor_settings: &ResolvedEditorSettings, + root: &Path, + configuration: Configuration, + ) -> anyhow::Result { + let settings = EditorConfigurationTransformer(editor_settings, root) + .transform(configuration) + .into_settings(root)?; + + Ok(RuffSettings { path: None, settings, - } + }) } } @@ -140,10 +189,11 @@ impl RuffSettingsIndex { Ok(Some(pyproject)) => { match ruff_workspace::resolver::resolve_root_settings( &pyproject, - Relativity::Parent, &EditorConfigurationTransformer(editor_settings, root), + ruff_workspace::resolver::ConfigurationOrigin::Ancestor, ) { Ok(settings) => { + tracing::debug!("Loaded settings from: `{}`", pyproject.display()); respect_gitignore = Some(settings.file_resolver.respect_gitignore); index.insert( @@ -264,10 +314,15 @@ impl RuffSettingsIndex { Ok(Some(pyproject)) => { match ruff_workspace::resolver::resolve_root_settings( &pyproject, - Relativity::Parent, &EditorConfigurationTransformer(editor_settings, root), + ruff_workspace::resolver::ConfigurationOrigin::Ancestor, ) { Ok(settings) => { + tracing::debug!( + "Loaded settings from: `{}` for `{}`", + pyproject.display(), + directory.display() + ); index.write().unwrap().insert( directory, Arc::new(RuffSettings { @@ -437,8 +492,8 @@ impl ConfigurationTransformer for EditorConfigurationTransformer<'_> { fn open_configuration_file(config_path: &Path) -> crate::Result { ruff_workspace::resolver::resolve_configuration( config_path, - Relativity::Cwd, &IdentityTransformer, + ruff_workspace::resolver::ConfigurationOrigin::UserSpecified, ) } diff --git a/crates/ruff_workspace/src/pyproject.rs b/crates/ruff_workspace/src/pyproject.rs index aa92b186bd..5693922eaf 100644 --- a/crates/ruff_workspace/src/pyproject.rs +++ b/crates/ruff_workspace/src/pyproject.rs @@ -42,18 +42,18 @@ impl Pyproject { /// Parse a `ruff.toml` file. fn parse_ruff_toml>(path: P) -> Result { - let contents = std::fs::read_to_string(path.as_ref()) - .with_context(|| format!("Failed to read {}", path.as_ref().display()))?; - toml::from_str(&contents) - .with_context(|| format!("Failed to parse {}", path.as_ref().display())) + let path = path.as_ref(); + let contents = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display())) } /// Parse a `pyproject.toml` file. fn parse_pyproject_toml>(path: P) -> Result { - let contents = std::fs::read_to_string(path.as_ref()) - .with_context(|| format!("Failed to read {}", path.as_ref().display()))?; - toml::from_str(&contents) - .with_context(|| format!("Failed to parse {}", path.as_ref().display())) + let path = path.as_ref(); + let contents = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display())) } /// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section. @@ -65,20 +65,21 @@ pub fn ruff_enabled>(path: P) -> Result { /// Return the path to the `pyproject.toml` or `ruff.toml` file in a given /// directory. pub fn settings_toml>(path: P) -> Result> { + let path = path.as_ref(); // Check for `.ruff.toml`. - let ruff_toml = path.as_ref().join(".ruff.toml"); + let ruff_toml = path.join(".ruff.toml"); if ruff_toml.is_file() { return Ok(Some(ruff_toml)); } // Check for `ruff.toml`. - let ruff_toml = path.as_ref().join("ruff.toml"); + let ruff_toml = path.join("ruff.toml"); if ruff_toml.is_file() { return Ok(Some(ruff_toml)); } // Check for `pyproject.toml`. - let pyproject_toml = path.as_ref().join("pyproject.toml"); + let pyproject_toml = path.join("pyproject.toml"); if pyproject_toml.is_file() && ruff_enabled(&pyproject_toml)? { return Ok(Some(pyproject_toml)); } @@ -97,6 +98,17 @@ pub fn find_settings_toml>(path: P) -> Result> { Ok(None) } +/// Derive target version from `required-version` in `pyproject.toml`, if +/// such a file exists in an ancestor directory. +pub fn find_fallback_target_version>(path: P) -> Option { + for directory in path.as_ref().ancestors() { + if let Some(fallback) = get_fallback_target_version(directory) { + return Some(fallback); + } + } + None +} + /// Find the path to the user-specific `pyproject.toml` or `ruff.toml`, if it /// exists. #[cfg(not(target_arch = "wasm32"))] @@ -141,9 +153,13 @@ pub fn find_user_settings_toml() -> Option { } /// Load `Options` from a `pyproject.toml` or `ruff.toml` file. -pub(super) fn load_options>(path: P) -> Result { - if path.as_ref().ends_with("pyproject.toml") { - let pyproject = parse_pyproject_toml(&path)?; +pub(super) fn load_options>( + path: P, + version_strategy: &TargetVersionStrategy, +) -> Result { + let path = path.as_ref(); + if path.ends_with("pyproject.toml") { + let pyproject = parse_pyproject_toml(path)?; let mut ruff = pyproject .tool .and_then(|tool| tool.ruff) @@ -157,16 +173,55 @@ pub(super) fn load_options>(path: P) -> Result { } Ok(ruff) } else { - let ruff = parse_ruff_toml(path); - if let Ok(ruff) = &ruff { + let mut ruff = parse_ruff_toml(path); + if let Ok(ref mut ruff) = ruff { if ruff.target_version.is_none() { - debug!("`project.requires_python` in `pyproject.toml` will not be used to set `target_version` when using `ruff.toml`."); + debug!("No `target-version` found in `ruff.toml`"); + match version_strategy { + TargetVersionStrategy::UseDefault => {} + TargetVersionStrategy::RequiresPythonFallback => { + if let Some(dir) = path.parent() { + let fallback = get_fallback_target_version(dir); + if let Some(version) = fallback { + debug!("Derived `target-version` from `requires-python` in `pyproject.toml`: {version:?}"); + } else { + debug!("No `pyproject.toml` with `requires-python` in same directory; `target-version` unspecified"); + } + ruff.target_version = fallback; + } + } + } } } ruff } } +/// Extract `target-version` from `pyproject.toml` in the given directory +/// if the file exists and has `requires-python`. +fn get_fallback_target_version(dir: &Path) -> Option { + let pyproject_path = dir.join("pyproject.toml"); + if !pyproject_path.exists() { + return None; + } + let parsed_pyproject = parse_pyproject_toml(&pyproject_path); + + let pyproject = match parsed_pyproject { + Ok(pyproject) => pyproject, + Err(err) => { + debug!("Failed to find fallback `target-version` due to: {}", err); + return None; + } + }; + + if let Some(project) = pyproject.project { + if let Some(requires_python) = project.requires_python { + return get_minimum_supported_version(&requires_python); + } + } + None +} + /// Infer the minimum supported [`PythonVersion`] from a `requires-python` specifier. fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option { /// Truncate a version to its major and minor components. @@ -199,6 +254,15 @@ fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option PythonVersion::iter().find(|version| Version::from(*version) == minimum_version) } +/// Strategy for handling missing `target-version` in configuration. +#[derive(Debug)] +pub(super) enum TargetVersionStrategy { + /// Use default `target-version` + UseDefault, + /// Derive from `requires-python` if available + RequiresPythonFallback, +} + #[cfg(test)] mod tests { use std::fs; diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 4c556a3fbc..1965f1c19a 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -23,7 +23,7 @@ use ruff_linter::package::PackageRoot; use ruff_linter::packaging::is_package; use crate::configuration::Configuration; -use crate::pyproject::settings_toml; +use crate::pyproject::{settings_toml, TargetVersionStrategy}; use crate::settings::Settings; use crate::{pyproject, FileResolverSettings}; @@ -301,9 +301,10 @@ pub trait ConfigurationTransformer { // resolving the "default" configuration). pub fn resolve_configuration( pyproject: &Path, - relativity: Relativity, transformer: &dyn ConfigurationTransformer, + origin: ConfigurationOrigin, ) -> Result { + let relativity = Relativity::from(origin); let mut configurations = indexmap::IndexMap::new(); let mut next = Some(fs::normalize_path(pyproject)); while let Some(path) = next { @@ -319,7 +320,19 @@ pub fn resolve_configuration( } // Resolve the current path. - let options = pyproject::load_options(&path).with_context(|| { + let version_strategy = + if configurations.is_empty() && matches!(origin, ConfigurationOrigin::Ancestor) { + // For configurations that are discovered by + // walking back from a file, we will attempt to + // infer the `target-version` if it is missing + TargetVersionStrategy::RequiresPythonFallback + } else { + // In all other cases (e.g. for configurations + // inherited via `extend`, or user-level settings) + // we do not attempt to infer a missing `target-version` + TargetVersionStrategy::UseDefault + }; + let options = pyproject::load_options(&path, &version_strategy).with_context(|| { if configurations.is_empty() { format!( "Failed to load configuration `{path}`", @@ -368,10 +381,12 @@ pub fn resolve_configuration( /// `pyproject.toml`. fn resolve_scoped_settings<'a>( pyproject: &'a Path, - relativity: Relativity, transformer: &dyn ConfigurationTransformer, + origin: ConfigurationOrigin, ) -> Result<(&'a Path, Settings)> { - let configuration = resolve_configuration(pyproject, relativity, transformer)?; + let relativity = Relativity::from(origin); + + let configuration = resolve_configuration(pyproject, transformer, origin)?; let project_root = relativity.resolve(pyproject); let settings = configuration.into_settings(project_root)?; Ok((project_root, settings)) @@ -381,13 +396,37 @@ fn resolve_scoped_settings<'a>( /// configuration with the given [`ConfigurationTransformer`]. pub fn resolve_root_settings( pyproject: &Path, - relativity: Relativity, transformer: &dyn ConfigurationTransformer, + origin: ConfigurationOrigin, ) -> Result { - let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, transformer)?; + let (_project_root, settings) = resolve_scoped_settings(pyproject, transformer, origin)?; Ok(settings) } +#[derive(Debug, Clone, Copy)] +/// How the configuration is provided. +pub enum ConfigurationOrigin { + /// Origin is unknown to the caller + Unknown, + /// User specified path to specific configuration file + UserSpecified, + /// User-level configuration (e.g. in `~/.config/ruff/pyproject.toml`) + UserSettings, + /// In parent or higher ancestor directory of path + Ancestor, +} + +impl From for Relativity { + fn from(value: ConfigurationOrigin) -> Self { + match value { + ConfigurationOrigin::Unknown => Self::Parent, + ConfigurationOrigin::UserSpecified => Self::Cwd, + ConfigurationOrigin::UserSettings => Self::Cwd, + ConfigurationOrigin::Ancestor => Self::Parent, + } + } +} + /// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths. pub fn python_files_in_path<'a>( paths: &[PathBuf], @@ -411,8 +450,11 @@ pub fn python_files_in_path<'a>( for ancestor in path.ancestors() { if seen.insert(ancestor) { if let Some(pyproject) = settings_toml(ancestor)? { - let (root, settings) = - resolve_scoped_settings(&pyproject, Relativity::Parent, transformer)?; + let (root, settings) = resolve_scoped_settings( + &pyproject, + transformer, + ConfigurationOrigin::Ancestor, + )?; resolver.add(root, settings); // We found the closest configuration. break; @@ -564,8 +606,8 @@ impl ParallelVisitor for PythonFilesVisitor<'_, '_> { match settings_toml(entry.path()) { Ok(Some(pyproject)) => match resolve_scoped_settings( &pyproject, - Relativity::Parent, self.transformer, + ConfigurationOrigin::Ancestor, ) { Ok((root, settings)) => { self.global.resolver.write().unwrap().add(root, settings); @@ -699,7 +741,7 @@ pub fn python_file_at_path( for ancestor in path.ancestors() { if let Some(pyproject) = settings_toml(ancestor)? { let (root, settings) = - resolve_scoped_settings(&pyproject, Relativity::Parent, transformer)?; + resolve_scoped_settings(&pyproject, transformer, ConfigurationOrigin::Unknown)?; resolver.add(root, settings); break; } @@ -883,7 +925,7 @@ mod tests { use crate::pyproject::find_settings_toml; use crate::resolver::{ is_file_excluded, match_exclusion, python_files_in_path, resolve_root_settings, - ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity, + ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, ResolvedFile, Resolver, }; use crate::settings::Settings; @@ -904,8 +946,8 @@ mod tests { PyprojectDiscoveryStrategy::Hierarchical, resolve_root_settings( &find_settings_toml(&package_root)?.unwrap(), - Relativity::Parent, &NoOpTransformer, + ConfigurationOrigin::Ancestor, )?, None, );