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
54 changes: 42 additions & 12 deletions crates/pyrefly_config/src/environment/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,34 +131,64 @@ impl PythonEnvironment {
);
}

let script = "\
let mut command = Command::new(interpreter);
command.arg("-c");
command.arg(Self::query_script());

Self::get_env_from_command(command, interpreter.display().to_string())
}

/// Query the environment that `uv` would create for a PEP 723 script.
pub fn get_env_from_uv_script(script: &Path) -> anyhow::Result<PythonEnvironment> {
let Some(script_dir) = script.parent() else {
return Err(anyhow!(
"Unable to query PEP 723 environment for `{}` because it has no parent directory",
script.display()
));
};

let mut command = Command::new("uv");
command.arg("run");
command.arg("--directory");
command.arg(script_dir);
command.arg("--no-project");
command.arg("--isolated");
command.arg("--with-requirements");
command.arg(script);
command.arg("python");
command.arg("-c");
command.arg(Self::query_script());

Self::get_env_from_command(command, format!("uv PEP 723 script {}", script.display()))
}

fn query_script() -> &'static str {
"\
import json, sys, sysconfig
platform = sys.platform
v = sys.version_info
version = '{}.{}.{}'.format(v.major, v.minor, v.micro)
stdlib_paths = [p for p in [sysconfig.get_path('stdlib')] if p is not None]
site_package_path = [p for p in sys.path if p != '' and '.zip' not in p and not p.endswith('/lib-dynload') and p not in stdlib_paths]
print(json.dumps({'python_platform': platform, 'python_version': version, 'site_package_path': site_package_path, 'stdlib_paths': stdlib_paths}))
";

let mut command = Command::new(interpreter);
command.arg("-c");
command.arg(script);
"
}

fn get_env_from_command(
mut command: Command,
command_description: String,
) -> anyhow::Result<PythonEnvironment> {
let python_info = command.output()?;

let stdout = String::from_utf8(python_info.stdout).with_context(|| {
format!(
"while parsing Python interpreter (`{}`) stdout for environment configuration",
interpreter.display()
)
format!("while parsing `{command_description}` stdout for environment configuration")
})?;
if !python_info.status.success() {
let stderr = String::from_utf8(python_info.stderr)
.unwrap_or("<Failed to parse STDOUT from UTF-8 string>".to_owned());
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback string is used when decoding stderr, but says "STDOUT". That makes error messages misleading when stderr isn’t valid UTF-8. Update it to refer to STDERR instead.

Suggested change
.unwrap_or("<Failed to parse STDOUT from UTF-8 string>".to_owned());
.unwrap_or("<Failed to parse STDERR from UTF-8 string>".to_owned());

Copilot uses AI. Check for mistakes.
return Err(anyhow::anyhow!(
"Unable to query interpreter {} for environment info:\nSTDOUT: {}\nSTDERR: {}",
interpreter.display(),
"Unable to query {} for environment info:\nSTDOUT: {}\nSTDERR: {}",
command_description,
stdout,
stderr
));
Expand Down
8 changes: 8 additions & 0 deletions crates/pyrefly_config/src/environment/interpreters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ impl Interpreters {
self.python_interpreter_path = Some(ConfigOrigin::lsp(interpreter));
}

/// Freeze interpreter discovery and clear any previously selected interpreter source.
pub fn disable_query(&mut self) {
self.skip_interpreter_query = true;
self.python_interpreter_path = None;
self.fallback_python_interpreter_name = None;
self.conda_environment = None;
}

/// Finds interpreters by searching in prioritized locations for the given project
/// and interpreter settings.
///
Expand Down
23 changes: 22 additions & 1 deletion crates/pyrefly_config/src/finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ type LoadCallback = Box<dyn Fn(&Path) -> (ArcId<ConfigFile>, Vec<ConfigError>) +
type FallbackCallback =
Box<dyn Fn(ModuleNameWithKind, &ModulePath) -> ArcId<ConfigFile> + Send + Sync>;

/// Function to augment a config for a specific Python file after it has been selected.
type AfterCallback = Box<
dyn Fn(ModuleNameWithKind, &ModulePath, ArcId<ConfigFile>) -> anyhow::Result<ArcId<ConfigFile>>
+ Send
+ Sync,
>;

/// A way to find a config file given a directory or Python file.
/// Uses a lot of caching.
pub struct ConfigFinder {
Expand All @@ -113,6 +120,8 @@ pub struct ConfigFinder {
/// If this returns a value, it is _not_ cached.
/// If this returns anything other than `Ok`, the rest of the functions are used.
before: BeforeCallback,
/// If this returns `Err`, the original config is returned and the error is recorded.
after: AfterCallback,
/// If there is no config file, or loading it fails, use this fallback.
fallback: FallbackCallback,
clear_extra_caches: Box<dyn Fn() + Send + Sync>,
Expand All @@ -128,6 +137,7 @@ impl ConfigFinder {
Self::new_custom(
Box::new(|_, _| Ok(None)),
load,
Box::new(|_, _, config| Ok(config)),
fallback,
clear_extra_caches,
)
Expand All @@ -142,6 +152,7 @@ impl ConfigFinder {
Self::new_custom(
Box::new(move |_, _| Ok(Some(c1.dupe()))),
Box::new(move |_| (c2.dupe(), Vec::new())),
Box::new(move |_, _, config| Ok(config)),
Box::new(move |_, _| c3.dupe()),
Box::new(|| {}),
)
Expand All @@ -153,6 +164,7 @@ impl ConfigFinder {
pub fn new_custom(
before: BeforeCallback,
load: LoadCallback,
after: AfterCallback,
fallback: FallbackCallback,
clear_extra_caches: Box<dyn Fn() + Send + Sync>,
) -> Self {
Expand Down Expand Up @@ -208,6 +220,7 @@ impl ConfigFinder {
),
errors,
before,
after,
fallback,
clear_extra_caches,
}
Expand Down Expand Up @@ -264,7 +277,7 @@ impl ConfigFinder {
None => (self.fallback)(name, path),
};

match path.details() {
let config = match path.details() {
ModulePathDetails::FileSystem(x) | ModulePathDetails::Memory(x) => {
let absolute = x.absolutize();
f(absolute.parent())
Expand All @@ -273,6 +286,14 @@ impl ConfigFinder {
ModulePathDetails::BundledTypeshed(_) => f(None),
ModulePathDetails::BundledTypeshedThirdParty(_) => f(None),
ModulePathDetails::BundledThirdParty(_) => f(None),
};

match (self.after)(name, path, config.dupe()) {
Ok(config) => config,
Err(e) => {
self.errors.lock().push(ConfigError::warn(e));
config
}
}
}

Expand Down
Loading
Loading