Skip to content

Commit 15c0535

Browse files
dylwil3MichaReiser
andauthored
Fallback to requires-python in certain cases when target-version is not found (#16319)
# Summary This PR introduces the following modifications in configuration resolution: 1. In the event where we are reading in a configuration file `myconfig` with no `target-version` specified, we will search for a `pyproject.toml` in the _same directory_ as `myconfig` and see if it has a `requires-python` field. If so, we will use that as the `target-version`. 2. In the event where... - we have not used the flag `--isolated`, and - we have not found any configuration file, and - we have not passed `--config` specifying a target version then we will search for a `pyproject.toml` file with `required-python` in an ancestor directory and use that if we find it. We've also added some debug logs to indicate which of these paths is taken. ## Implementation Two small things: 1. I have chosen a method that will sometimes re-parse a `pyproject.toml` file that has already been parsed at some earlier stage in the resolution process. It seemed like avoiding that would require more drastic changes - but maybe there is a clever way that I'm not seeing! 2. When searching for these fallbacks, I suppress any errors that may occur when parsing `pyproject.toml`s rather than propagate them. The reasoning here is that we have already found or decided upon a perfectly good configuration, and this is just a "bonus" to find a better guess for the target version. Closes #14813, #16662 ## Testing The linked issue contains a repo for reproducing the behavior, which we used for a manual test: ```console ruff-F821-repro on  main ❯ uvx ruff check --no-cache hello.py:9:11: F821 Undefined name `anext`. Consider specifying `requires-python = ">= 3.10"` or `tool.ruff.target-version = "py310"` in your `pyproject.toml` file. | 8 | async def main(): 9 | print(anext(g())) | ^^^^^ F821 | Found 1 error. ruff-F821-repro on  main ❯ ../ruff/target/debug/ruff check --no-cache All checks passed! ``` In addition, we've added snapshot tests with the CLI output in some examples. Please let me know if there are some additional scenarios you'd like me to add tests for! --------- Co-authored-by: Micha Reiser <[email protected]>
1 parent d622137 commit 15c0535

File tree

10 files changed

+2914
-65
lines changed

10 files changed

+2914
-65
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ toml = { version = "0.8.11" }
154154
tracing = { version = "0.1.40" }
155155
tracing-flame = { version = "0.2.0" }
156156
tracing-indicatif = { version = "0.3.6" }
157+
tracing-log = { version = "0.2.0" }
157158
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
158159
"env-filter",
159160
"fmt",

crates/ruff/src/lib.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ pub fn run(
153153
}));
154154
}
155155

156-
set_up_logging(global_options.log_level())?;
156+
// Don't set up logging for the server command, as it has its own logging setup
157+
// and setting the global logger can only be done once.
158+
if !matches!(command, Command::Server { .. }) {
159+
set_up_logging(global_options.log_level())?;
160+
}
157161

158162
match command {
159163
Command::Version { output_format } => {

crates/ruff/src/resolve.rs

+55-7
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ use log::debug;
55
use path_absolutize::path_dedot;
66

77
use ruff_workspace::configuration::Configuration;
8-
use ruff_workspace::pyproject;
8+
use ruff_workspace::pyproject::{self, find_fallback_target_version};
99
use ruff_workspace::resolver::{
10-
resolve_root_settings, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy,
11-
Relativity,
10+
resolve_root_settings, ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig,
11+
PyprojectDiscoveryStrategy,
1212
};
1313

14+
use ruff_python_ast as ast;
15+
1416
use crate::args::ConfigArguments;
1517

1618
/// Resolve the relevant settings strategy and defaults for the current
@@ -35,7 +37,11 @@ pub fn resolve(
3537
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
3638
// current working directory. (This matches ESLint's behavior.)
3739
if let Some(pyproject) = config_arguments.config_file() {
38-
let settings = resolve_root_settings(pyproject, Relativity::Cwd, config_arguments)?;
40+
let settings = resolve_root_settings(
41+
pyproject,
42+
config_arguments,
43+
ConfigurationOrigin::UserSpecified,
44+
)?;
3945
debug!(
4046
"Using user-specified configuration file at: {}",
4147
pyproject.display()
@@ -61,7 +67,8 @@ pub fn resolve(
6167
"Using configuration file (via parent) at: {}",
6268
pyproject.display()
6369
);
64-
let settings = resolve_root_settings(&pyproject, Relativity::Parent, config_arguments)?;
70+
let settings =
71+
resolve_root_settings(&pyproject, config_arguments, ConfigurationOrigin::Ancestor)?;
6572
return Ok(PyprojectConfig::new(
6673
PyprojectDiscoveryStrategy::Hierarchical,
6774
settings,
@@ -74,11 +81,35 @@ pub fn resolve(
7481
// end up the "closest" `pyproject.toml` file for every Python file later on, so
7582
// these act as the "default" settings.)
7683
if let Some(pyproject) = pyproject::find_user_settings_toml() {
84+
struct FallbackTransformer<'a> {
85+
arguments: &'a ConfigArguments,
86+
}
87+
88+
impl ConfigurationTransformer for FallbackTransformer<'_> {
89+
fn transform(&self, mut configuration: Configuration) -> Configuration {
90+
// The `requires-python` constraint from the `pyproject.toml` takes precedence
91+
// over the `target-version` from the user configuration.
92+
let fallback = find_fallback_target_version(&*path_dedot::CWD);
93+
if let Some(fallback) = fallback {
94+
debug!("Derived `target-version` from found `requires-python`: {fallback:?}");
95+
configuration.target_version = Some(fallback.into());
96+
}
97+
98+
self.arguments.transform(configuration)
99+
}
100+
}
101+
77102
debug!(
78103
"Using configuration file (via cwd) at: {}",
79104
pyproject.display()
80105
);
81-
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, config_arguments)?;
106+
let settings = resolve_root_settings(
107+
&pyproject,
108+
&FallbackTransformer {
109+
arguments: config_arguments,
110+
},
111+
ConfigurationOrigin::UserSettings,
112+
)?;
82113
return Ok(PyprojectConfig::new(
83114
PyprojectDiscoveryStrategy::Hierarchical,
84115
settings,
@@ -91,7 +122,24 @@ pub fn resolve(
91122
// "closest" `pyproject.toml` file for every Python file later on, so these act
92123
// as the "default" settings.)
93124
debug!("Using Ruff default settings");
94-
let config = config_arguments.transform(Configuration::default());
125+
let mut config = config_arguments.transform(Configuration::default());
126+
if config.target_version.is_none() {
127+
// If we have arrived here we know that there was no `pyproject.toml`
128+
// containing a `[tool.ruff]` section found in an ancestral directory.
129+
// (This is an implicit requirement in the function
130+
// `pyproject::find_settings_toml`.)
131+
// However, there may be a `pyproject.toml` with a `requires-python`
132+
// specified, and that is what we look for in this step.
133+
let fallback = find_fallback_target_version(
134+
stdin_filename
135+
.as_ref()
136+
.unwrap_or(&path_dedot::CWD.as_path()),
137+
);
138+
if let Some(version) = fallback {
139+
debug!("Derived `target-version` from found `requires-python`: {version:?}");
140+
}
141+
config.target_version = fallback.map(ast::PythonVersion::from);
142+
}
95143
let settings = config.into_settings(&path_dedot::CWD)?;
96144
Ok(PyprojectConfig::new(
97145
PyprojectDiscoveryStrategy::Hierarchical,

0 commit comments

Comments
 (0)