From 6b64630635688429bb16a1fad5ca5fb8eebd375c Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 12 May 2025 15:39:04 -0500 Subject: [PATCH] Update `--python` to accept paths to executables in virtual environments (#17954) ## Summary Updates the `--python` flag to accept Python executables in virtual environments. Notably, we do not query the executable and it _must_ be in a canonical location in a virtual environment. This is pretty naive, but solves for the trivial case of `ty check --python .venv/bin/python3` which will be a common mistake (and `ty check --python $(which python)`) I explored this while trying to understand Python discovery in ty in service of https://github.com/astral-sh/ty/issues/272, I'm not attached to it, but figure it's worth sharing. As an alternative, we can add more variants to the `SearchPathValidationError` and just improve the _error_ message, i.e., by hinting that this looks like a virtual environment and suggesting the concrete alternative path they should provide. We'll probably want to do that for some other cases anyway (e.g., `3.13` as described in the linked issue) This functionality is also briefly mentioned in https://github.com/astral-sh/ty/issues/193 Closes https://github.com/astral-sh/ty/issues/318 ## Test Plan e.g., ``` uv run ty check --python .venv/bin/python3 ``` needs test coverage still --- crates/ty/docs/cli.md | 9 ++++--- crates/ty/src/args.rs | 16 +++++++---- .../src/module_resolver/resolver.rs | 27 +++++++++++++++++++ crates/ty_python_semantic/src/program.rs | 5 +++- crates/ty_test/src/config.rs | 13 ++++++--- 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/crates/ty/docs/cli.md b/crates/ty/docs/cli.md index e83f0ccc95..4df73846cb 100644 --- a/crates/ty/docs/cli.md +++ b/crates/ty/docs/cli.md @@ -56,10 +56,11 @@ ty check [OPTIONS] [PATH]...
--project project

Run the command within the given project directory.

All pyproject.toml files will be discovered by walking up the directory tree from the given project directory, as will the project's virtual environment (.venv) unless the venv-path option is set.

Other command-line arguments (such as relative paths) will be resolved relative to the current working directory.

-
--python path

Path to the Python installation from which ty resolves type information and third-party dependencies.

-

If not specified, ty will look at the VIRTUAL_ENV environment variable.

-

ty will search in the path's site-packages directories for type information and third-party imports.

-

This option is commonly used to specify the path to a virtual environment.

+
--python path

Path to the Python environment.

+

ty uses the Python environment to resolve type information and third-party dependencies.

+

If not specified, ty will attempt to infer it from the VIRTUAL_ENV environment variable or discover a .venv directory in the project root or working directory.

+

If a path to a Python interpreter is provided, e.g., .venv/bin/python3, ty will attempt to find an environment two directories up from the interpreter's path, e.g., .venv. At this time, ty does not invoke the interpreter to determine the location of the environment. This means that ty will not resolve dynamic executables such as a shim.

+

ty will search in the resolved environments's site-packages directories for type information and third-party imports.

--python-platform, --platform platform

Target platform to assume when resolving types.

This is used to specialize the type of sys.platform and will affect the visibility of platform-specific functions and attributes. If the value is set to all, no assumptions are made about the target platform. If unspecified, the current system's platform will be used.

--python-version, --target-version version

Python version to assume when resolving types

diff --git a/crates/ty/src/args.rs b/crates/ty/src/args.rs index c4e8988e80..be65581580 100644 --- a/crates/ty/src/args.rs +++ b/crates/ty/src/args.rs @@ -51,14 +51,20 @@ pub(crate) struct CheckCommand { #[arg(long, value_name = "PROJECT")] pub(crate) project: Option, - /// Path to the Python installation from which ty resolves type information and third-party dependencies. + /// Path to the Python environment. /// - /// If not specified, ty will look at the `VIRTUAL_ENV` environment variable. + /// ty uses the Python environment to resolve type information and third-party dependencies. /// - /// ty will search in the path's `site-packages` directories for type information and - /// third-party imports. + /// If not specified, ty will attempt to infer it from the `VIRTUAL_ENV` environment variable or + /// discover a `.venv` directory in the project root or working directory. /// - /// This option is commonly used to specify the path to a virtual environment. + /// If a path to a Python interpreter is provided, e.g., `.venv/bin/python3`, ty will attempt to + /// find an environment two directories up from the interpreter's path, e.g., `.venv`. At this + /// time, ty does not invoke the interpreter to determine the location of the environment. This + /// means that ty will not resolve dynamic executables such as a shim. + /// + /// ty will search in the resolved environments's `site-packages` directories for type + /// information and third-party imports. #[arg(long, value_name = "PATH")] pub(crate) python: Option, diff --git a/crates/ty_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs index 0be7d5107e..6b2c76e23a 100644 --- a/crates/ty_python_semantic/src/module_resolver/resolver.rs +++ b/crates/ty_python_semantic/src/module_resolver/resolver.rs @@ -247,6 +247,33 @@ impl SearchPaths { .and_then(|env| env.site_packages_directories(system))? } + PythonPath::Resolve(target, origin) => { + tracing::debug!("Resolving {origin}: {target}"); + + let root = system + // If given a file, assume it's a Python executable, e.g., `.venv/bin/python3`, + // and search for a virtual environment in the root directory. Ideally, we'd + // invoke the target to determine `sys.prefix` here, but that's more complicated + // and may be deferred to uv. + .is_file(target) + .then(|| target.as_path()) + .take_if(|target| { + // Avoid using the target if it doesn't look like a Python executable, e.g., + // to deny cases like `.venv/bin/foo` + target + .file_name() + .is_some_and(|name| name.starts_with("python")) + }) + .and_then(SystemPath::parent) + .and_then(SystemPath::parent) + // If not a file, use the path as given and allow let `PythonEnvironment::new` + // handle the error. + .unwrap_or(target); + + PythonEnvironment::new(root, *origin, system) + .and_then(|venv| venv.site_packages_directories(system))? + } + PythonPath::Discover(root) => { tracing::debug!("Discovering virtual environment in `{root}`"); let virtual_env_path = discover_venv_in(db.system(), root); diff --git a/crates/ty_python_semantic/src/program.rs b/crates/ty_python_semantic/src/program.rs index 63bc35be57..5683b62073 100644 --- a/crates/ty_python_semantic/src/program.rs +++ b/crates/ty_python_semantic/src/program.rs @@ -145,6 +145,9 @@ pub enum PythonPath { /// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix SysPrefix(SystemPathBuf, SysPrefixPathOrigin), + /// Resolve a path to an executable (or environment directory) into a usable environment. + Resolve(SystemPathBuf, SysPrefixPathOrigin), + /// Tries to discover a virtual environment in the given path. Discover(SystemPathBuf), @@ -161,6 +164,6 @@ impl PythonPath { } pub fn from_cli_flag(path: SystemPathBuf) -> Self { - Self::SysPrefix(path, SysPrefixPathOrigin::PythonCliFlag) + Self::Resolve(path, SysPrefixPathOrigin::PythonCliFlag) } } diff --git a/crates/ty_test/src/config.rs b/crates/ty_test/src/config.rs index ba27900719..8bcaef829b 100644 --- a/crates/ty_test/src/config.rs +++ b/crates/ty_test/src/config.rs @@ -68,12 +68,17 @@ pub(crate) struct Environment { /// Additional search paths to consider when resolving modules. pub(crate) extra_paths: Option>, - /// Path to the Python installation from which ty resolves type information and third-party dependencies. + /// Path to the Python environment. /// - /// ty will search in the path's `site-packages` directories for type information and - /// third-party imports. + /// ty uses the Python environment to resolve type information and third-party dependencies. /// - /// This option is commonly used to specify the path to a virtual environment. + /// If a path to a Python interpreter is provided, e.g., `.venv/bin/python3`, ty will attempt to + /// find an environment two directories up from the interpreter's path, e.g., `.venv`. At this + /// time, ty does not invoke the interpreter to determine the location of the environment. This + /// means that ty will not resolve dynamic executables such as a shim. + /// + /// ty will search in the resolved environment's `site-packages` directories for type + /// information and third-party imports. #[serde(skip_serializing_if = "Option::is_none")] pub python: Option, }