From 29c95f7ef35a26b6f10c50c85b88490985b31767 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Tue, 28 Jul 2026 16:04:32 +0900 Subject: [PATCH] fix --- Cargo.lock | 19 +++++++++++ crates/pyrefly_config/Cargo.toml | 1 + crates/pyrefly_config/src/config.rs | 52 +++++++++++++++++++++++++++++ schemas/pyrefly.json | 5 +++ schemas/test-pyproject.toml | 3 ++ schemas/test-pyrefly.toml | 3 ++ website/docs/configuration.mdx | 24 +++++++++++++ 7 files changed, 107 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 9fffd6e063..7145705ab1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2003,6 +2003,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pep440_rs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c" +dependencies = [ + "once_cell", + "serde", + "unicode-width", + "unscanny", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2287,6 +2299,7 @@ dependencies = [ "enum-iterator", "itertools 0.15.0", "parse-display", + "pep440_rs", "pretty_assertions", "pulldown-cmark", "pyrefly_build", @@ -3598,6 +3611,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" +[[package]] +name = "unscanny" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" + [[package]] name = "url" version = "2.5.8" diff --git a/crates/pyrefly_config/Cargo.toml b/crates/pyrefly_config/Cargo.toml index ca514ed7d2..4f92b07b93 100644 --- a/crates/pyrefly_config/Cargo.toml +++ b/crates/pyrefly_config/Cargo.toml @@ -18,6 +18,7 @@ dupe = "0.9.1" enum-iterator = "2.3.0" itertools = "0.15.0" parse-display = "0.8.2" +pep440_rs = "0.7.3" pyrefly_build = { path = "../pyrefly_build" } pyrefly_python = { path = "../pyrefly_python" } pyrefly_util = { path = "../pyrefly_util" } diff --git a/crates/pyrefly_config/src/config.rs b/crates/pyrefly_config/src/config.rs index b8cfc3684c..122a34fdcb 100644 --- a/crates/pyrefly_config/src/config.rs +++ b/crates/pyrefly_config/src/config.rs @@ -23,6 +23,8 @@ use clap::ValueEnum; use derivative::Derivative; use dupe::Dupe as _; use itertools::Itertools; +use pep440_rs::Version; +use pep440_rs::VersionSpecifiers; use pyrefly_build::BuildSystem; use pyrefly_build::handle::Handle; use pyrefly_build::source_db::SourceDatabase; @@ -520,6 +522,9 @@ pub struct ConfigFile { #[serde(skip)] pub source: ConfigSource, + /// The PEP 440 version requirement that the running Pyrefly must satisfy. + pub required_version: Option, + /// Files that should be counted as sources (e.g. user-space code). /// NOTE: unlike other args, this is never replaced with CLI arg overrides /// in this config, but may be overridden by CLI args where used. @@ -685,6 +690,7 @@ impl Default for ConfigFile { fn default() -> Self { ConfigFile { source: ConfigSource::Synthetic, + required_version: None, project_includes: Default::default(), project_excludes: Default::default(), interpreters: Interpreters { @@ -1695,6 +1701,25 @@ impl ConfigFile { ))); } + if let Some(required_version) = &config.required_version { + match required_version.parse::() { + Ok(specifiers) => { + let running_version = env!("CARGO_PKG_VERSION"); + let parsed_running_version = running_version + .parse::() + .expect("Pyrefly's package version must be PEP 440 compatible"); + if !specifiers.contains(&parsed_running_version) { + errors.push(ConfigError::error(anyhow!( + "Pyrefly {running_version} does not satisfy `required-version = \"{required_version}\"`" + ))); + } + } + Err(error) => errors.push(ConfigError::error(anyhow!( + "Invalid `required-version` `{required_version}`: {error}" + ))), + } + } + if !config.root.extras.0.is_empty() { let extra_keys = config.root.extras.0.keys().join(", "); errors.push(ConfigError::warn(anyhow!( @@ -1947,6 +1972,7 @@ mod tests { config, ConfigFile { source: ConfigSource::Synthetic, + required_version: None, project_includes: Globs::new(vec![ "tests".to_owned(), "./implementation".to_owned() @@ -2335,6 +2361,7 @@ mod tests { let interpreter = "venv/bin/python3".to_owned(); let mut config = ConfigFile { source: ConfigSource::Synthetic, + required_version: None, project_includes: Globs::new(vec!["path1/**".to_owned(), "path2/path3".to_owned()]) .unwrap(), project_excludes: Globs::new(vec!["tests/untyped/**".to_owned()]).unwrap(), @@ -2409,6 +2436,7 @@ mod tests { let expected_config = ConfigFile { source: ConfigSource::Synthetic, + required_version: None, project_includes: Globs::new(project_includes_vec).unwrap(), project_excludes: Globs::new(project_excludes_vec).unwrap(), interpreters: Interpreters { @@ -3877,6 +3905,30 @@ output-format = "omit-errors" assert_eq!(config.source.root(), Some(root.path())); } + #[test] + fn test_required_version() { + let root = TempDir::new().unwrap(); + let path = root.path().join(ConfigFile::PYREFLY_FILE_NAME); + for (required_version, expect_error) in [ + (format!("=={}", env!("CARGO_PKG_VERSION")), false), + ("<0".to_owned(), true), + ("not a specifier".to_owned(), true), + ] { + fs::write(&path, format!("required-version = {required_version:?}")).unwrap(); + let (config, errors) = ConfigFile::from_file(&path); + assert_eq!( + config.required_version.as_deref(), + Some(required_version.as_str()) + ); + if expect_error { + assert_eq!(errors.len(), 1); + assert_eq!(errors[0].severity(), Severity::Error); + } else { + assert!(errors.is_empty()); + } + } + } + #[test] fn test_explicit_search_path_wins_over_site_packages() { // An explicit search path should take priority over a site-package diff --git a/schemas/pyrefly.json b/schemas/pyrefly.json index ad5af6c5e0..1423cdf254 100644 --- a/schemas/pyrefly.json +++ b/schemas/pyrefly.json @@ -105,6 +105,11 @@ { "type": "object", "properties": { + "required-version": { + "description": "A PEP 440 version specifier that the running version of Pyrefly must satisfy.", + "type": "string", + "examples": [">=1.2.0", ">=1.2.0,<2.0.0", "==1.2.0"] + }, "preset": { "description": "Named preset that provides default error severities and behavior settings. User-specified settings override the preset.", "type": "string", diff --git a/schemas/test-pyproject.toml b/schemas/test-pyproject.toml index 80e3c62702..bd3952ed8a 100644 --- a/schemas/test-pyproject.toml +++ b/schemas/test-pyproject.toml @@ -5,6 +5,9 @@ name = "example-project" version = "0.1.0" [tool.pyrefly] +# Runtime version requirement +required-version = ">=0.0.0.dev0" + # Basic project configuration project-includes = ["src", "tests"] project-excludes = ["**/node_modules", "**/__pycache__"] diff --git a/schemas/test-pyrefly.toml b/schemas/test-pyrefly.toml index 9dc83633ca..da2cc57b4a 100644 --- a/schemas/test-pyrefly.toml +++ b/schemas/test-pyrefly.toml @@ -4,6 +4,9 @@ # Preset (named collection of defaults applied as the base config) preset = "legacy" +# Runtime version requirement +required-version = ">=0.0.0.dev0" + # Basic project configuration project-includes = ["src", "tests"] project-excludes = ["**/node_modules", "**/__pycache__"] diff --git a/website/docs/configuration.mdx b/website/docs/configuration.mdx index 7aa93743ea..1d0a5fa456 100644 --- a/website/docs/configuration.mdx +++ b/website/docs/configuration.mdx @@ -548,6 +548,30 @@ set on the CLI. - `"sarif"` emits a SARIF 2.1.0 report suitable for static-analysis integrations. +### `required-version` + +Enforces a requirement on the version of Pyrefly at runtime. Pyrefly reports a +configuration error if its version does not satisfy the requirement. This can +keep local CLI runs, editor integrations, and CI on compatible versions. + +```toml +# pyrefly.toml +required-version = ">=1.2.0,<2.0.0" +``` + +```toml +# pyproject.toml +[tool.pyrefly] +required-version = ">=1.2.0,<2.0.0" +``` + +- Type: string containing a [PEP 440 version specifier](https://peps.python.org/pep-0440/#version-specifiers) +- Default: none +- Flag equivalent: none +- Notes: + - `required-version` is a **project-level setting** and cannot be overridden + in [`sub-config`](#sub-configs) sections. + ### `preset` A named collection of error severities and behavior settings that serves as the base configuration.