From c4e651921ba1911a04102e2052c9fe56f185309d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 9 Aug 2024 16:49:17 +0100 Subject: [PATCH] [red-knot] Move, rename and make public the `PyVersion` type (#12782) --- crates/red_knot_python_semantic/src/lib.rs | 4 +- .../src/module_resolver/resolver.rs | 3 +- .../src/module_resolver/testing.rs | 3 +- .../src/module_resolver/typeshed/versions.rs | 89 +++--------- .../red_knot_python_semantic/src/program.rs | 54 +------ .../src/python_version.rs | 136 ++++++++++++++++++ .../src/semantic_model.rs | 3 +- .../src/types/infer.rs | 3 +- 8 files changed, 167 insertions(+), 128 deletions(-) create mode 100644 crates/red_knot_python_semantic/src/python_version.rs diff --git a/crates/red_knot_python_semantic/src/lib.rs b/crates/red_knot_python_semantic/src/lib.rs index bae2308900..bd1daf7719 100644 --- a/crates/red_knot_python_semantic/src/lib.rs +++ b/crates/red_knot_python_semantic/src/lib.rs @@ -5,7 +5,8 @@ use rustc_hash::FxHasher; pub use db::Db; pub use module_name::ModuleName; pub use module_resolver::{resolve_module, system_module_search_paths, vendored_typeshed_stubs}; -pub use program::{Program, ProgramSettings, SearchPathSettings, TargetVersion}; +pub use program::{Program, ProgramSettings, SearchPathSettings}; +pub use python_version::{PythonVersion, TargetVersion, UnsupportedPythonVersion}; pub use semantic_model::{HasTy, SemanticModel}; pub mod ast_node_ref; @@ -15,6 +16,7 @@ mod module_name; mod module_resolver; mod node_key; mod program; +mod python_version; pub mod semantic_index; mod semantic_model; pub mod types; diff --git a/crates/red_knot_python_semantic/src/module_resolver/resolver.rs b/crates/red_knot_python_semantic/src/module_resolver/resolver.rs index fa3222b57a..7b4ea2a385 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/resolver.rs +++ b/crates/red_knot_python_semantic/src/module_resolver/resolver.rs @@ -499,9 +499,8 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, Mod let resolver_settings = module_resolution_settings(db); let target_version = resolver_settings.target_version(); let resolver_state = ResolverState::new(db, target_version); - let (_, minor_version) = target_version.as_tuple(); let is_builtin_module = - ruff_python_stdlib::sys::is_builtin_module(minor_version, name.as_str()); + ruff_python_stdlib::sys::is_builtin_module(target_version.minor_version(), name.as_str()); for search_path in resolver_settings.search_paths(db) { // When a builtin module is imported, standard module resolution is bypassed: diff --git a/crates/red_knot_python_semantic/src/module_resolver/testing.rs b/crates/red_knot_python_semantic/src/module_resolver/testing.rs index 218761c16f..628a702e13 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/testing.rs +++ b/crates/red_knot_python_semantic/src/module_resolver/testing.rs @@ -2,7 +2,8 @@ use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf}; use ruff_db::vendored::VendoredPathBuf; use crate::db::tests::TestDb; -use crate::program::{Program, SearchPathSettings, TargetVersion}; +use crate::program::{Program, SearchPathSettings}; +use crate::python_version::TargetVersion; /// A test case for the module resolver. /// diff --git a/crates/red_knot_python_semantic/src/module_resolver/typeshed/versions.rs b/crates/red_knot_python_semantic/src/module_resolver/typeshed/versions.rs index 778dedf7d6..6962953f12 100644 --- a/crates/red_knot_python_semantic/src/module_resolver/typeshed/versions.rs +++ b/crates/red_knot_python_semantic/src/module_resolver/typeshed/versions.rs @@ -14,7 +14,7 @@ use ruff_db::files::{system_path_to_file, File}; use super::vendored::vendored_typeshed_stubs; use crate::db::Db; use crate::module_name::ModuleName; -use crate::TargetVersion; +use crate::python_version::{PythonVersion, TargetVersion}; #[derive(Debug)] pub(crate) struct LazyTypeshedVersions<'db>(OnceCell<&'db TypeshedVersions>); @@ -63,7 +63,7 @@ impl<'db> LazyTypeshedVersions<'db> { // Unwrapping here is not correct... parse_typeshed_versions(db, versions_file).as_ref().unwrap() }); - versions.query_module(module, PyVersion::from(target_version)) + versions.query_module(module, PythonVersion::from(target_version)) } } @@ -177,7 +177,7 @@ impl TypeshedVersions { fn query_module( &self, module: &ModuleName, - target_version: PyVersion, + target_version: PythonVersion, ) -> TypeshedVersionsQueryResult { if let Some(range) = self.exact(module) { if range.contains(target_version) { @@ -322,13 +322,13 @@ impl fmt::Display for TypeshedVersions { #[derive(Debug, Clone, Eq, PartialEq, Hash)] enum PyVersionRange { - AvailableFrom(RangeFrom), - AvailableWithin(RangeInclusive), + AvailableFrom(RangeFrom), + AvailableWithin(RangeInclusive), } impl PyVersionRange { #[must_use] - fn contains(&self, version: PyVersion) -> bool { + fn contains(&self, version: PythonVersion) -> bool { match self { Self::AvailableFrom(inner) => inner.contains(&version), Self::AvailableWithin(inner) => inner.contains(&version), @@ -342,9 +342,14 @@ impl FromStr for PyVersionRange { fn from_str(s: &str) -> Result { let mut parts = s.split('-').map(str::trim); match (parts.next(), parts.next(), parts.next()) { - (Some(lower), Some(""), None) => Ok(Self::AvailableFrom((lower.parse()?)..)), + (Some(lower), Some(""), None) => { + let lower = PythonVersion::from_versions_file_string(lower)?; + Ok(Self::AvailableFrom(lower..)) + } (Some(lower), Some(upper), None) => { - Ok(Self::AvailableWithin((lower.parse()?)..=(upper.parse()?))) + let lower = PythonVersion::from_versions_file_string(lower)?; + let upper = PythonVersion::from_versions_file_string(upper)?; + Ok(Self::AvailableWithin(lower..=upper)) } _ => Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfHyphens), } @@ -362,74 +367,20 @@ impl fmt::Display for PyVersionRange { } } -#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] -struct PyVersion { - major: u8, - minor: u8, -} - -impl FromStr for PyVersion { - type Err = TypeshedVersionsParseErrorKind; - - fn from_str(s: &str) -> Result { +impl PythonVersion { + fn from_versions_file_string(s: &str) -> Result { let mut parts = s.split('.').map(str::trim); let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else { return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods( s.to_string(), )); }; - let major = match u8::from_str(major) { - Ok(major) => major, - Err(err) => { - return Err(TypeshedVersionsParseErrorKind::IntegerParsingFailure { - version: s.to_string(), - err, - }) + PythonVersion::try_from((major, minor)).map_err(|int_parse_error| { + TypeshedVersionsParseErrorKind::IntegerParsingFailure { + version: s.to_string(), + err: int_parse_error, } - }; - let minor = match u8::from_str(minor) { - Ok(minor) => minor, - Err(err) => { - return Err(TypeshedVersionsParseErrorKind::IntegerParsingFailure { - version: s.to_string(), - err, - }) - } - }; - Ok(Self { major, minor }) - } -} - -impl fmt::Display for PyVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let PyVersion { major, minor } = self; - write!(f, "{major}.{minor}") - } -} - -impl From for PyVersion { - fn from(value: TargetVersion) -> Self { - match value { - TargetVersion::Py37 => PyVersion { major: 3, minor: 7 }, - TargetVersion::Py38 => PyVersion { major: 3, minor: 8 }, - TargetVersion::Py39 => PyVersion { major: 3, minor: 9 }, - TargetVersion::Py310 => PyVersion { - major: 3, - minor: 10, - }, - TargetVersion::Py311 => PyVersion { - major: 3, - minor: 11, - }, - TargetVersion::Py312 => PyVersion { - major: 3, - minor: 12, - }, - TargetVersion::Py313 => PyVersion { - major: 3, - minor: 13, - }, - } + }) } } diff --git a/crates/red_knot_python_semantic/src/program.rs b/crates/red_knot_python_semantic/src/program.rs index 22b3dfa68c..00b225cedb 100644 --- a/crates/red_knot_python_semantic/src/program.rs +++ b/crates/red_knot_python_semantic/src/program.rs @@ -1,3 +1,4 @@ +use crate::python_version::TargetVersion; use crate::Db; use ruff_db::system::SystemPathBuf; use salsa::Durability; @@ -24,59 +25,6 @@ pub struct ProgramSettings { pub search_paths: SearchPathSettings, } -/// Enumeration of all supported Python versions -/// -/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates? -#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] -pub enum TargetVersion { - Py37, - #[default] - Py38, - Py39, - Py310, - Py311, - Py312, - Py313, -} - -impl TargetVersion { - pub const fn as_tuple(self) -> (u8, u8) { - match self { - Self::Py37 => (3, 7), - Self::Py38 => (3, 8), - Self::Py39 => (3, 9), - Self::Py310 => (3, 10), - Self::Py311 => (3, 11), - Self::Py312 => (3, 12), - Self::Py313 => (3, 13), - } - } - - const fn as_str(self) -> &'static str { - match self { - Self::Py37 => "py37", - Self::Py38 => "py38", - Self::Py39 => "py39", - Self::Py310 => "py310", - Self::Py311 => "py311", - Self::Py312 => "py312", - Self::Py313 => "py313", - } - } -} - -impl std::fmt::Display for TargetVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.as_str()) - } -} - -impl std::fmt::Debug for TargetVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(self, f) - } -} - /// Configures the search paths for module resolution. #[derive(Eq, PartialEq, Debug, Clone, Default)] pub struct SearchPathSettings { diff --git a/crates/red_knot_python_semantic/src/python_version.rs b/crates/red_knot_python_semantic/src/python_version.rs new file mode 100644 index 0000000000..84f73488ce --- /dev/null +++ b/crates/red_knot_python_semantic/src/python_version.rs @@ -0,0 +1,136 @@ +use std::fmt; + +/// Enumeration of all supported Python versions +/// +/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates? +#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] +pub enum TargetVersion { + Py37, + #[default] + Py38, + Py39, + Py310, + Py311, + Py312, + Py313, +} + +impl TargetVersion { + pub fn major_version(self) -> u8 { + PythonVersion::from(self).major + } + + pub fn minor_version(self) -> u8 { + PythonVersion::from(self).minor + } + + const fn as_display_str(self) -> &'static str { + match self { + Self::Py37 => "py37", + Self::Py38 => "py38", + Self::Py39 => "py39", + Self::Py310 => "py310", + Self::Py311 => "py311", + Self::Py312 => "py312", + Self::Py313 => "py313", + } + } +} + +impl fmt::Display for TargetVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_display_str()) + } +} + +impl fmt::Debug for TargetVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +/// Generic representation for a Python version. +/// +/// Unlike [`TargetVersion`], this does not necessarily represent +/// a Python version that we actually support. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct PythonVersion { + pub major: u8, + pub minor: u8, +} + +impl TryFrom<(&str, &str)> for PythonVersion { + type Error = std::num::ParseIntError; + + fn try_from(value: (&str, &str)) -> Result { + let (major, minor) = value; + Ok(Self { + major: major.parse()?, + minor: minor.parse()?, + }) + } +} + +impl fmt::Display for PythonVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let PythonVersion { major, minor } = self; + write!(f, "{major}.{minor}") + } +} + +impl From for PythonVersion { + fn from(value: TargetVersion) -> Self { + match value { + TargetVersion::Py37 => PythonVersion { major: 3, minor: 7 }, + TargetVersion::Py38 => PythonVersion { major: 3, minor: 8 }, + TargetVersion::Py39 => PythonVersion { major: 3, minor: 9 }, + TargetVersion::Py310 => PythonVersion { + major: 3, + minor: 10, + }, + TargetVersion::Py311 => PythonVersion { + major: 3, + minor: 11, + }, + TargetVersion::Py312 => PythonVersion { + major: 3, + minor: 12, + }, + TargetVersion::Py313 => PythonVersion { + major: 3, + minor: 13, + }, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct UnsupportedPythonVersion(PythonVersion); + +impl fmt::Display for UnsupportedPythonVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Python version {} is unsupported", self.0) + } +} + +impl std::error::Error for UnsupportedPythonVersion {} + +impl TryFrom for TargetVersion { + type Error = UnsupportedPythonVersion; + + fn try_from(value: PythonVersion) -> Result { + let PythonVersion { major: 3, minor } = value else { + return Err(UnsupportedPythonVersion(value)); + }; + match minor { + 7 => Ok(TargetVersion::Py37), + 8 => Ok(TargetVersion::Py38), + 9 => Ok(TargetVersion::Py39), + 10 => Ok(TargetVersion::Py310), + 11 => Ok(TargetVersion::Py311), + 12 => Ok(TargetVersion::Py312), + 13 => Ok(TargetVersion::Py313), + _ => Err(UnsupportedPythonVersion(value)), + } + } +} diff --git a/crates/red_knot_python_semantic/src/semantic_model.rs b/crates/red_knot_python_semantic/src/semantic_model.rs index 602777e102..ef9916ae3a 100644 --- a/crates/red_knot_python_semantic/src/semantic_model.rs +++ b/crates/red_knot_python_semantic/src/semantic_model.rs @@ -168,7 +168,8 @@ mod tests { use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use crate::db::tests::TestDb; - use crate::program::{Program, SearchPathSettings, TargetVersion}; + use crate::program::{Program, SearchPathSettings}; + use crate::python_version::TargetVersion; use crate::types::Type; use crate::{HasTy, SemanticModel}; diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 29b9c7ce16..2f24430465 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -1502,7 +1502,8 @@ mod tests { use crate::builtins::builtins_scope; use crate::db::tests::TestDb; - use crate::program::{Program, SearchPathSettings, TargetVersion}; + use crate::program::{Program, SearchPathSettings}; + use crate::python_version::TargetVersion; use crate::semantic_index::definition::Definition; use crate::semantic_index::symbol::FileScopeId; use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};