Compare commits

...

2 Commits

Author SHA1 Message Date
Zanie Blue
4a90f33781 Move to --no-active-virtual-env flag 2025-12-17 13:55:03 -06:00
Zanie Blue
1f8b187aa1 [ty] Add TY_IGNORE_ACTIVE_VIRTUAL_ENV to ignore VIRTUAL_ENV 2025-12-17 12:09:33 -06:00
6 changed files with 172 additions and 10 deletions

View File

@@ -175,6 +175,13 @@ pub(crate) struct CheckCommand {
/// For example, spinners or progress bars.
#[arg(global = true, long, value_parser = clap::builder::BoolishValueParser::new(), help_heading = "Global options")]
pub no_progress: bool,
/// Ignore the `VIRTUAL_ENV` environment variable when discovering the Python environment.
///
/// This is useful when a tool wrapping ty activates a virtual environment but you would
/// prefer to discover the `.venv` in the working directory.
#[arg(long, hide = true)]
pub(crate) ignore_active_virtual_env: bool,
}
impl CheckCommand {

View File

@@ -114,6 +114,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let system = OsSystem::new(&cwd);
let watch = args.watch;
let exit_zero = args.exit_zero;
let ignore_active_virtual_env = args.ignore_active_virtual_env;
let config_file = args
.config_file
.as_ref()
@@ -125,6 +126,7 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
};
project_metadata.apply_configuration_files(&system)?;
project_metadata.set_ignore_active_virtual_env(ignore_active_virtual_env);
let project_options_overrides = ProjectOptionsOverrides::new(config_file, args.into_options());
project_metadata.apply_overrides(&project_options_overrides);

View File

@@ -2703,3 +2703,123 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> {
Ok(())
}
/// When `--ignore-active-virtual-env` is passed, ty should ignore the `VIRTUAL_ENV`
/// environment variable and fall back to other discovery mechanisms.
#[test]
fn ignore_active_virtual_env() -> anyhow::Result<()> {
let active_venv_package1_path = if cfg!(windows) {
"myvenv/Lib/site-packages/package1/__init__.py"
} else {
"myvenv/lib/python3.13/site-packages/package1/__init__.py"
};
let working_venv_package1_path = if cfg!(windows) {
"project/.venv/Lib/site-packages/package1/__init__.py"
} else {
"project/.venv/lib/python3.13/site-packages/package1/__init__.py"
};
let case = CliTest::with_files([
(
"project/test.py",
r#"
from package1 import ActiveVenv
from package1 import WorkingVenv
"#,
),
(
"project/.venv/pyvenv.cfg",
r#"
home = ./
"#,
),
(
"myvenv/pyvenv.cfg",
r#"
home = ./
"#,
),
(
active_venv_package1_path,
r#"
class WorkingVenv: ...
"#,
),
(
working_venv_package1_path,
r#"
class ActiveVenv: ...
"#,
),
])?;
// With `VIRTUAL_ENV` set, the active environment is found
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("VIRTUAL_ENV", case.root().join("myvenv")), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `ActiveVenv`
--> test.py:2:22
|
2 | from package1 import ActiveVenv
| ^^^^^^^^^^
3 | from package1 import WorkingVenv
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// With `--ignore-active-virtual-env`, we ignore `VIRTUAL_ENV` and find the `.venv` instead
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("VIRTUAL_ENV", case.root().join("myvenv"))
.arg("--ignore-active-virtual-env"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import WorkingVenv
| ^^^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
// We still find `.venv` when activated and `--ignore-active-virtual-env` is passed
assert_cmd_snapshot!(case.command()
.current_dir(case.root().join("project"))
.env("VIRTUAL_ENV", case.root().join(".venv"))
.arg("--ignore-active-virtual-env"), @r###"
success: false
exit_code: 1
----- stdout -----
error[unresolved-import]: Module `package1` has no member `WorkingVenv`
--> test.py:3:22
|
2 | from package1 import ActiveVenv
3 | from package1 import WorkingVenv
| ^^^^^^^^^^^
|
info: rule `unresolved-import` is enabled by default
Found 1 diagnostic
----- stderr -----
"###);
Ok(())
}

View File

@@ -37,6 +37,11 @@ pub struct ProjectMetadata {
/// The path ordering doesn't imply precedence.
#[cfg_attr(test, serde(skip_serializing_if = "Vec::is_empty"))]
pub(super) extra_configuration_paths: Vec<SystemPathBuf>,
/// If true, ignore the `VIRTUAL_ENV` environment variable when discovering
/// the Python environment.
#[cfg_attr(test, serde(skip_serializing_if = "std::ops::Not::not"))]
pub(super) ignore_active_virtual_env: bool,
}
impl ProjectMetadata {
@@ -47,6 +52,7 @@ impl ProjectMetadata {
root,
extra_configuration_paths: Vec::default(),
options: Options::default(),
ignore_active_virtual_env: false,
}
}
@@ -70,6 +76,7 @@ impl ProjectMetadata {
root: system.current_directory().to_path_buf(),
options,
extra_configuration_paths: vec![path],
ignore_active_virtual_env: false,
})
}
@@ -117,6 +124,7 @@ impl ProjectMetadata {
root,
options,
extra_configuration_paths: Vec::new(),
ignore_active_virtual_env: false,
})
}
@@ -268,13 +276,22 @@ impl ProjectMetadata {
&self.extra_configuration_paths
}
pub fn set_ignore_active_virtual_env(&mut self, ignore: bool) {
self.ignore_active_virtual_env = ignore;
}
pub fn to_program_settings(
&self,
system: &dyn System,
vendored: &VendoredFileSystem,
) -> anyhow::Result<ProgramSettings> {
self.options
.to_program_settings(self.root(), self.name(), system, vendored)
self.options.to_program_settings(
self.root(),
self.name(),
system,
vendored,
self.ignore_active_virtual_env,
)
}
pub fn apply_overrides(&mut self, overrides: &ProjectOptionsOverrides) {

View File

@@ -117,6 +117,7 @@ impl Options {
project_name: &str,
system: &dyn System,
vendored: &VendoredFileSystem,
ignore_active_virtual_env: bool,
) -> anyhow::Result<ProgramSettings> {
let environment = self.environment.or_default();
@@ -160,7 +161,7 @@ impl Options {
system,
)?)
} else {
PythonEnvironment::discover(project_root, system)
PythonEnvironment::discover(project_root, system, ignore_active_virtual_env)
.context("Failed to discover local Python environment")?
};

View File

@@ -163,6 +163,7 @@ impl PythonEnvironment {
pub fn discover(
project_root: &SystemPath,
system: &dyn System,
ignore_active_virtual_env: bool,
) -> Result<Option<Self>, SitePackagesDiscoveryError> {
fn resolve_environment(
system: &dyn System,
@@ -173,13 +174,9 @@ impl PythonEnvironment {
PythonEnvironment::new(path, origin, system)
}
if let Ok(virtual_env) = system.env_var(EnvVars::VIRTUAL_ENV) {
return resolve_environment(
system,
SystemPath::new(&virtual_env),
SysPrefixPathOrigin::VirtualEnvVar,
)
.map(Some);
if let Some(virtual_env) = virtual_environment_from_env(system, ignore_active_virtual_env) {
return resolve_environment(system, &virtual_env, SysPrefixPathOrigin::VirtualEnvVar)
.map(Some);
}
if let Some(conda_env) = conda_environment_from_env(system, CondaEnvironmentKind::Child) {
@@ -696,6 +693,24 @@ pub(crate) fn conda_environment_from_env(
Some(path)
}
/// Read `VIRTUAL_ENV`.
///
/// Returns `None` if `ignore` is `true`.
pub(crate) fn virtual_environment_from_env(
system: &dyn System,
ignore: bool,
) -> Option<SystemPathBuf> {
if ignore {
tracing::debug!(
"Ignoring active virtual environment (from `VIRTUAL_ENV`) due to `--ignore-active-virtual-env`"
);
return None;
}
let dir = system.env_var(EnvVars::VIRTUAL_ENV).ok()?;
Some(SystemPathBuf::from(dir))
}
/// A parser for `pyvenv.cfg` files: metadata files for virtual environments.
///
/// Note that a `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax!