ruff/crates/ty_python_semantic/src/program.rs

208 lines
6.8 KiB
Rust

use std::sync::Arc;
use crate::Db;
use crate::module_resolver::SearchPaths;
use crate::python_platform::PythonPlatform;
use crate::site_packages::SysPrefixPathOrigin;
use anyhow::Context;
use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_python_ast::PythonVersion;
use ruff_text_size::TextRange;
use salsa::Durability;
use salsa::Setter;
#[salsa::input(singleton)]
pub struct Program {
#[returns(ref)]
pub python_version_with_source: PythonVersionWithSource,
#[returns(ref)]
pub python_platform: PythonPlatform,
#[returns(ref)]
pub(crate) search_paths: SearchPaths,
}
impl Program {
pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> anyhow::Result<Self> {
let ProgramSettings {
python_version: python_version_with_source,
python_platform,
search_paths,
} = settings;
tracing::info!(
"Python version: Python {python_version}, platform: {python_platform}",
python_version = python_version_with_source.version
);
let search_paths = SearchPaths::from_settings(db, &search_paths)
.with_context(|| "Invalid search path settings")?;
Ok(
Program::builder(python_version_with_source, python_platform, search_paths)
.durability(Durability::HIGH)
.new(db),
)
}
pub fn python_version(self, db: &dyn Db) -> PythonVersion {
self.python_version_with_source(db).version
}
pub fn update_from_settings(
self,
db: &mut dyn Db,
settings: ProgramSettings,
) -> anyhow::Result<()> {
let ProgramSettings {
python_version,
python_platform,
search_paths,
} = settings;
if &python_platform != self.python_platform(db) {
tracing::debug!("Updating python platform: `{python_platform:?}`");
self.set_python_platform(db).to(python_platform);
}
if &python_version != self.python_version_with_source(db) {
tracing::debug!("Updating python version: `{python_version:?}`");
self.set_python_version_with_source(db).to(python_version);
}
self.update_search_paths(db, &search_paths)?;
Ok(())
}
pub fn update_search_paths(
self,
db: &mut dyn Db,
search_path_settings: &SearchPathSettings,
) -> anyhow::Result<()> {
let search_paths = SearchPaths::from_settings(db, search_path_settings)?;
if self.search_paths(db) != &search_paths {
tracing::debug!("Update search paths");
self.set_search_paths(db).to(search_paths);
}
Ok(())
}
pub fn custom_stdlib_search_path(self, db: &dyn Db) -> Option<&SystemPath> {
self.search_paths(db).custom_stdlib()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProgramSettings {
pub python_version: PythonVersionWithSource,
pub python_platform: PythonPlatform,
pub search_paths: SearchPathSettings,
}
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub enum PythonVersionSource {
/// Value loaded from a project's configuration file.
File(Arc<SystemPathBuf>, Option<TextRange>),
/// The value comes from a CLI argument, while it's left open if specified using a short argument,
/// long argument (`--extra-paths`) or `--config key=value`.
Cli,
/// We fell back to a default value because the value was not specified via the CLI or a config file.
#[default]
Default,
}
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct PythonVersionWithSource {
pub version: PythonVersion,
pub source: PythonVersionSource,
}
impl Default for PythonVersionWithSource {
fn default() -> Self {
Self {
version: PythonVersion::latest_ty(),
source: PythonVersionSource::Default,
}
}
}
/// Configures the search paths for module resolution.
#[derive(Eq, PartialEq, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct SearchPathSettings {
/// List of user-provided paths that should take first priority in the module resolution.
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
/// or pyright's stubPath configuration setting.
pub extra_paths: Vec<SystemPathBuf>,
/// The root of the project, used for finding first-party modules.
pub src_roots: Vec<SystemPathBuf>,
/// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types.
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
/// bundled as a zip file in the binary
pub custom_typeshed: Option<SystemPathBuf>,
/// Path to the Python installation from which ty resolves third party dependencies
/// and their type information.
pub python_path: PythonPath,
}
impl SearchPathSettings {
pub fn new(src_roots: Vec<SystemPathBuf>) -> Self {
Self {
src_roots,
extra_paths: vec![],
custom_typeshed: None,
python_path: PythonPath::KnownSitePackages(vec![]),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum PythonPath {
/// A path that represents the value of [`sys.prefix`] at runtime in Python
/// for a given Python executable.
///
/// For the case of a virtual environment, where a
/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to
/// the virtual environment the Python binary lies inside, i.e. `/.venv`,
/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`.
/// System Python installations generally work the same way: if a system
/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix`
/// will be `/opt/homebrew`, and `site-packages` will be at
/// `/opt/homebrew/lib/python3.X/site-packages`.
///
/// [`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),
/// Resolved site packages paths.
///
/// This variant is mainly intended for testing where we want to skip resolving `site-packages`
/// because it would unnecessarily complicate the test setup.
KnownSitePackages(Vec<SystemPathBuf>),
}
impl PythonPath {
pub fn from_virtual_env_var(path: impl Into<SystemPathBuf>) -> Self {
Self::SysPrefix(path.into(), SysPrefixPathOrigin::VirtualEnvVar)
}
pub fn from_cli_flag(path: SystemPathBuf) -> Self {
Self::Resolve(path, SysPrefixPathOrigin::PythonCliFlag)
}
}