use std::sync::Arc; use crate::Db; use crate::module_resolver::{SearchPathValidationError, SearchPaths}; use crate::python_platform::PythonPlatform; use ruff_db::diagnostic::Span; use ruff_db::files::system_path_to_file; use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_db::vendored::VendoredFileSystem; 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 init_or_update(db: &mut dyn Db, settings: ProgramSettings) -> Self { match Self::try_get(db) { Some(program) => { program.update_from_settings(db, settings); program } None => Self::from_settings(db, settings), } } pub fn from_settings(db: &dyn Db, settings: ProgramSettings) -> Self { let ProgramSettings { python_version, python_platform, search_paths, } = settings; search_paths.try_register_static_roots(db); Program::builder(python_version, 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) { let ProgramSettings { python_version, python_platform, search_paths, } = settings; if self.search_paths(db) != &search_paths { tracing::debug!("Updating search paths"); search_paths.try_register_static_roots(db); self.set_search_paths(db).to(search_paths); } 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}", version = python_version.version ); self.set_python_version_with_source(db).to(python_version); } } 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: SearchPaths, } #[derive(Clone, Debug, Eq, PartialEq, Default)] pub enum PythonVersionSource { /// Value loaded from a project's configuration file. ConfigFile(PythonVersionFileSource), /// Value loaded from the `pyvenv.cfg` file of the virtual environment. /// The virtual environment might have been configured, activated or inferred. PyvenvCfgFile(PythonVersionFileSource), /// Value inferred from the layout of the Python installation. /// /// This only ever applies on Unix. On Unix, the `site-packages` directory /// will always be at `sys.prefix/lib/pythonX.Y/site-packages`, /// so we can infer the Python version from the parent directory of `site-packages`. InstallationDirectoryLayout { site_packages_parent_dir: Box }, /// 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, } /// Information regarding the file and [`TextRange`] of the configuration /// from which we inferred the Python version. #[derive(Debug, PartialEq, Eq, Clone)] pub struct PythonVersionFileSource { path: Arc, range: Option, } impl PythonVersionFileSource { pub fn new(path: Arc, range: Option) -> Self { Self { path, range } } /// Attempt to resolve a [`Span`] that corresponds to the location of /// the configuration setting that specified the Python version. /// /// Useful for subdiagnostics when informing the user /// what the inferred Python version of their project is. pub(crate) fn span(&self, db: &dyn Db) -> Option { let file = system_path_to_file(db, &*self.path).ok()?; Some(Span::from(file).with_optional_range(self.range)) } } #[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)] 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, /// The root of the project, used for finding first-party modules. pub src_roots: Vec, /// 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, /// List of site packages paths to use. pub site_packages_paths: Vec, } impl SearchPathSettings { pub fn new(src_roots: Vec) -> Self { Self { src_roots, ..SearchPathSettings::empty() } } pub fn empty() -> Self { SearchPathSettings { src_roots: vec![], extra_paths: vec![], custom_typeshed: None, site_packages_paths: vec![], } } pub fn to_search_paths( &self, system: &dyn System, vendored: &VendoredFileSystem, ) -> Result { SearchPaths::from_settings(self, system, vendored) } }