Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/pyrefly_config/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,24 +186,28 @@ impl EnvironmentArgs {
config.interpreters.skip_interpreter_query = true;
config.interpreters.python_interpreter_path = None;
config.interpreters.fallback_python_interpreter_name = None;
config.interpreters.python_interpreter_find_cmd = None;
config.interpreters.conda_environment = None;
}
if let Some(x) = &self.python_interpreter_path {
config.interpreters.python_interpreter_path = Some(ConfigOrigin::cli(x.clone()));
config.interpreters.fallback_python_interpreter_name = None;
config.interpreters.python_interpreter_find_cmd = None;
config.interpreters.conda_environment = None;
}
if let Some(x) = &self.fallback_python_interpreter_name {
config.interpreters.fallback_python_interpreter_name =
Some(ConfigOrigin::cli(x.clone()));
config.interpreters.python_interpreter_path = None;
config.interpreters.python_interpreter_find_cmd = None;
config.interpreters.conda_environment = None;
}
if let Some(conda_environment) = &self.conda_environment {
config.interpreters.conda_environment =
Some(ConfigOrigin::cli(conda_environment.clone()));
config.interpreters.python_interpreter_path = None;
config.interpreters.fallback_python_interpreter_name = None;
config.interpreters.python_interpreter_find_cmd = None;
}
if let Some(x) = &self.typeshed_path {
config.typeshed_path = Some(x.clone());
Expand Down
140 changes: 109 additions & 31 deletions crates/pyrefly_config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ use crate::finder::ConfigError;
use crate::migration::run::MigratedFromKind;
use crate::module_wildcard::Match;
use crate::pyproject::PyProject;
use crate::util::ConfigOrigin;

pub static GENERATED_FILE_CONFIG_OVERRIDE: LazyLock<
RwLock<SmallMap<InternedPath, ArcId<ConfigFile>>>,
Expand Down Expand Up @@ -703,6 +704,7 @@ impl Default for ConfigFile {
interpreters: Interpreters {
python_interpreter_path: None,
fallback_python_interpreter_name: None,
python_interpreter_find_cmd: None,
conda_environment: None,
skip_interpreter_query: false,
},
Expand Down Expand Up @@ -1346,16 +1348,35 @@ impl ConfigFile {
// file or CLI flag). If not, we auto-discover a `typings/` directory below.
let site_package_path_set = self.python_environment.site_package_path.is_some();

let mut interpreter_selections = Vec::new();
if matches!(
self.interpreters.python_interpreter_path.as_ref(),
Some(ConfigOrigin::CommandLine(_) | ConfigOrigin::ConfigFile(_))
) {
interpreter_selections.push("python-interpreter-path");
}
if self.interpreters.python_interpreter_find_cmd.is_some() {
interpreter_selections.push("python-interpreter-find-cmd");
}
if self.interpreters.fallback_python_interpreter_name.is_some() {
interpreter_selections.push("fallback-python-interpreter-name");
}
if self.interpreters.conda_environment.is_some() {
interpreter_selections.push("conda-environment");
}
if self.interpreters.skip_interpreter_query {
interpreter_selections.push("skip-interpreter-query");
}
if interpreter_selections.len() > 1 {
configure_errors.push(anyhow::anyhow!(
"Only one interpreter selection option can be set, but found: {}.",
interpreter_selections.join(", ")
));
}

if self.interpreters.skip_interpreter_query {
self.python_environment.set_empty_to_default();
} else {
if self.interpreters.python_interpreter_path.is_some()
&& self.interpreters.fallback_python_interpreter_name.is_some()
{
configure_errors.push(anyhow::anyhow!(
"`python-interpreter-path` and `fallback-python-interpreter-name` both set, but only one can be used."
));
}
match self.interpreters.find_interpreter(project_root.as_deref()) {
Ok(interpreter) => {
let (env, error) = PythonEnvironment::get_interpreter_env(&interpreter);
Expand Down Expand Up @@ -1606,14 +1627,6 @@ impl ConfigFile {
}
configure_errors.extend(validate(&self.search_path_from_file, "search-path"));

if self.interpreters.python_interpreter_path.is_some()
&& self.interpreters.conda_environment.is_some()
{
configure_errors.push(anyhow::anyhow!(
"Cannot use both `python-interpreter-path` and `conda-environment`. Finding environment info using `python-interpreter-path`.",
));
}

if let ConfigSource::File(path) = &self.source {
configure_errors
.into_map(|e| ConfigError::warn(e.context(format!("{}", path.display()))))
Expand Down Expand Up @@ -1934,6 +1947,7 @@ mod tests {
use super::*;
use crate::base::ExtraConfigs;
use crate::base::UntypedDefBehavior;
use crate::environment::interpreters::InterpreterDiscoveryCommand;
use crate::error_kind::ErrorKind;
use crate::error_kind::Severity;
use crate::module_wildcard::ModuleWildcard;
Expand Down Expand Up @@ -2017,6 +2031,7 @@ mod tests {
"venv/my/python"
))),
fallback_python_interpreter_name: None,
python_interpreter_find_cmd: None,
conda_environment: None,
skip_interpreter_query: false,
},
Expand Down Expand Up @@ -2391,6 +2406,7 @@ mod tests {
interpreter.clone(),
))),
fallback_python_interpreter_name: None,
python_interpreter_find_cmd: None,
conda_environment: None,
skip_interpreter_query: false,
},
Expand Down Expand Up @@ -2452,6 +2468,7 @@ mod tests {
interpreters: Interpreters {
python_interpreter_path: Some(ConfigOrigin::config(test_path.join(interpreter))),
fallback_python_interpreter_name: None,
python_interpreter_find_cmd: None,
conda_environment: None,
skip_interpreter_query: false,
},
Expand Down Expand Up @@ -2604,6 +2621,28 @@ output-format = "omit-errors"
assert_eq!(config.output_format, Some(OutputFormat::FullTextWithGithub));
}

#[test]
fn test_python_interpreter_find_cmd_config_parsing() {
let config = ConfigFile::parse_config(
r#"python-interpreter-find-cmd = ["poetry", "env", "info", "-e"]"#,
)
.unwrap();
let expected = ["poetry", "env", "info", "-e"].map(str::to_owned);
assert_eq!(
config.interpreters.python_interpreter_find_cmd.as_deref(),
Some(expected.as_slice())
);
let serialized = toml::to_string(&config).unwrap();
assert_eq!(ConfigFile::parse_config(&serialized).unwrap(), config);

let error = ConfigFile::parse_config(r#"python-interpreter-find-cmd = []"#).unwrap_err();
assert!(
error
.to_string()
.contains("`python-interpreter-find-cmd` must contain a program")
);
}

#[test]
fn test_expect_all_fields_set_in_root_config() {
let root = TempDir::new().unwrap();
Expand All @@ -2619,6 +2658,7 @@ output-format = "omit-errors"
"project-excludes",
"python-interpreter-path",
"fallback-python-interpreter-name",
"python-interpreter-find-cmd",
// values we won't be getting
"extras",
// values that must be Some (if flattened, their contents will be checked)
Expand Down Expand Up @@ -3521,24 +3561,61 @@ output-format = "omit-errors"
}

#[test]
fn test_python_interpreter_conda_environment() {
let mut config = ConfigFile {
interpreters: Interpreters {
python_interpreter_path: Some(ConfigOrigin::config(PathBuf::new())),
fallback_python_interpreter_name: None,
conda_environment: Some(ConfigOrigin::config("".to_owned())),
skip_interpreter_query: false,
},
..Default::default()
};
fn test_interpreter_selection_options_are_mutually_exclusive() {
let selections = [
"python-interpreter-path",
"python-interpreter-find-cmd",
"fallback-python-interpreter-name",
"conda-environment",
"skip-interpreter-query",
];

let validation_errors = config.configure();
for (first_index, first) in selections.iter().enumerate() {
for second in &selections[first_index + 1..] {
let mut interpreters = Interpreters::default();
for selection in [first, second] {
match *selection {
"python-interpreter-path" => {
interpreters.python_interpreter_path =
Some(ConfigOrigin::config(PathBuf::from("ignored")));
}
"python-interpreter-find-cmd" => {
interpreters.python_interpreter_find_cmd = Some(
InterpreterDiscoveryCommand::try_from(vec!["ignored".to_owned()])
.unwrap(),
);
}
"fallback-python-interpreter-name" => {
interpreters.fallback_python_interpreter_name =
Some(ConfigOrigin::config("ignored".to_owned()));
}
"conda-environment" => {
interpreters.conda_environment =
Some(ConfigOrigin::config("ignored".to_owned()));
}
"skip-interpreter-query" => {
interpreters.skip_interpreter_query = true;
}
_ => unreachable!("all interpreter selections are covered"),
}
}

assert!(
validation_errors.iter().any(|e| {
e.get_message() == "Cannot use both `python-interpreter-path` and `conda-environment`. Finding environment info using `python-interpreter-path`."
})
);
let mut config = ConfigFile {
interpreters,
..Default::default()
};
let expected = format!(
"Only one interpreter selection option can be set, but found: {first}, {second}."
);
assert!(
config
.configure()
.iter()
.any(|error| error.get_message() == expected),
"missing validation error for {first} and {second}"
);
}
}
}

#[test]
Expand All @@ -3562,6 +3639,7 @@ output-format = "omit-errors"
interpreters: Interpreters {
python_interpreter_path: Some(ConfigOrigin::config(PathBuf::from("abcd"))),
fallback_python_interpreter_name: None,
python_interpreter_find_cmd: None,
conda_environment: None,
skip_interpreter_query: false,
},
Expand Down
Loading
Loading