Skip to content

Commit cda2212

Browse files
martindemellometa-codesync[bot]
authored andcommitted
Add a target python version option to lifeguard
Summary: Prerequisite for supporting the new `lazy` keyword, since we want to gate that to 3.15+. Also addresses the fact that open source users will not have a single default python version. Enforces a minimum python version of 3.12. Reviewed By: brittanyrey Differential Revision: D107935421 fbshipit-source-id: bd66f43346fe12d2399aceccde4684ba456a48a0
1 parent 9f73c18 commit cda2212

18 files changed

Lines changed: 294 additions & 51 deletions

src/commands/analyze.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use pyrefly_python::module_name::ModuleName;
1515
use tracing::info;
1616

1717
use crate::debug::report_peak_memory;
18+
use crate::runner::DEFAULT_PYTHON_VERSION;
1819
use crate::runner::Options;
20+
use crate::runner::parse_python_version;
1921
use crate::runner::process_source_map;
2022
use crate::source_map;
2123
use crate::tracing::ProcessTimer;
@@ -55,6 +57,10 @@ pub struct AnalyzeArgs {
5557
/// Name of the main module (the module run as __main__)
5658
#[arg(long = "main-module")]
5759
pub main_module: Option<String>,
60+
61+
/// Python version to use for parsing
62+
#[arg(long = "python-version", default_value = DEFAULT_PYTHON_VERSION)]
63+
pub python_version: String,
5864
}
5965

6066
pub fn run(args: AnalyzeArgs) -> Result<()> {
@@ -78,10 +84,13 @@ pub fn run(args: AnalyzeArgs) -> Result<()> {
7884
None => std::env::current_dir()?,
7985
};
8086

87+
let python_version = parse_python_version(&args.python_version)?;
88+
8189
let options = Options {
8290
verbose_output_path: args.verbose_output_path,
8391
sorted_output: args.sorted_output,
8492
main_module: args.main_module.map(|s| ModuleName::from_str(&s)),
93+
python_version,
8594
};
8695

8796
let lifeguard_output = process_source_map(src_map, &root_dir, &options)?;

src/commands/analyze_binary.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use tracing::info;
1818
use crate::cache::LibraryCache;
1919
use crate::debug::report_peak_memory;
2020
use crate::output::LifeGuardAnalysis;
21+
use crate::runner::DEFAULT_PYTHON_VERSION;
2122
use crate::runner::Options;
23+
use crate::runner::parse_python_version;
2224
use crate::tracing::ProcessTimer;
2325
use crate::tracing::time;
2426

@@ -38,6 +40,10 @@ pub struct AnalyzeBinaryArgs {
3840
/// Name of the main module (the module run as __main__)
3941
#[arg(long = "main-module")]
4042
pub main_module: Option<String>,
43+
44+
/// Python version to use for parsing
45+
#[arg(long = "python-version", default_value = DEFAULT_PYTHON_VERSION)]
46+
pub python_version: String,
4147
}
4248

4349
pub fn run(args: AnalyzeBinaryArgs) -> Result<()> {
@@ -68,10 +74,13 @@ pub fn run(args: AnalyzeBinaryArgs) -> Result<()> {
6874

6975
info!("Merged cache: {} modules", merged.modules.len());
7076

77+
let python_version = parse_python_version(&args.python_version)?;
78+
7179
let options = Options {
7280
verbose_output_path: None,
7381
sorted_output: args.sorted_output,
7482
main_module: args.main_module.map(|s| ModuleName::from_str(&s)),
83+
python_version,
7584
};
7685

7786
let analysis = time("Building analysis from cache", || {

src/commands/analyze_library.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ use tracing::warn;
1616
use crate::cache::LibraryCache;
1717
use crate::debug::report_peak_memory;
1818
use crate::project::CachingMode;
19+
use crate::runner::DEFAULT_PYTHON_VERSION;
20+
use crate::runner::Options;
21+
use crate::runner::parse_python_version;
1922
use crate::runner::run_pipeline;
2023
use crate::source_map;
2124
use crate::source_map::SourceMap;
@@ -35,6 +38,10 @@ pub struct AnalyzeLibraryArgs {
3538
/// Cross-library resolution is handled entirely in the reduce step (analyze-binary).
3639
#[arg(long = "dep-cache", hide = true)]
3740
pub dep_caches: Vec<PathBuf>,
41+
42+
/// Python version to use for parsing
43+
#[arg(long = "python-version", default_value = DEFAULT_PYTHON_VERSION)]
44+
pub python_version: String,
3845
}
3946

4047
/// Detect the root directory by walking up from cwd until a source file resolves.
@@ -91,13 +98,22 @@ pub fn run(args: AnalyzeLibraryArgs) -> Result<()> {
9198
source_map::load_source_map(&args.db_path)
9299
})?;
93100

101+
let python_version = parse_python_version(&args.python_version)?;
102+
103+
let options = Options {
104+
verbose_output_path: None,
105+
sorted_output: false,
106+
main_module: None,
107+
python_version,
108+
};
109+
94110
let cache = if src_map.is_empty() {
95111
info!("Source map is empty, producing empty cache");
96112
LibraryCache::empty()
97113
} else {
98114
let root_dir = detect_root_dir(&src_map)?;
99115

100-
let result = run_pipeline(src_map, &root_dir, CachingMode::Enabled, None)?;
116+
let result = run_pipeline(src_map, &root_dir, CachingMode::Enabled, &options)?;
101117

102118
time("Building cache", || {
103119
LibraryCache::build(

src/commands/gen_source_db.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use clap::Parser;
1515
use serde::Serialize;
1616

1717
use crate::find_sources::build_source_db;
18+
use crate::runner::DEFAULT_PYTHON_VERSION;
19+
use crate::runner::parse_python_version;
1820

1921
#[derive(Parser)]
2022
pub struct GenSourceDbArgs {
@@ -27,6 +29,10 @@ pub struct GenSourceDbArgs {
2729
/// Path to site-packages directory (overrides pyproject.toml setting)
2830
#[arg(long)]
2931
site_packages: Option<PathBuf>,
32+
33+
/// Python version to use for parsing
34+
#[arg(long = "python-version", default_value = DEFAULT_PYTHON_VERSION)]
35+
python_version: String,
3036
}
3137

3238
#[derive(Serialize)]
@@ -35,7 +41,12 @@ struct SourceDb {
3541
}
3642

3743
pub fn run(args: GenSourceDbArgs) -> Result<()> {
38-
let (build_map, seed_count) = build_source_db(&args.input_dir, args.site_packages.as_deref())?;
44+
let python_version = parse_python_version(&args.python_version)?;
45+
let (build_map, seed_count) = build_source_db(
46+
&args.input_dir,
47+
args.site_packages.as_deref(),
48+
python_version,
49+
)?;
3950
eprintln!(
4051
"Seeded with {} files from {}",
4152
seed_count,

src/commands/run_tree.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ use pyrefly_python::module_name::ModuleName;
1515

1616
use crate::find_sources::build_source_db;
1717
use crate::find_sources::make_source_map;
18+
use crate::runner::DEFAULT_PYTHON_VERSION;
1819
use crate::runner::Options;
20+
use crate::runner::parse_python_version;
1921
use crate::runner::process_source_map;
2022
use crate::tracing::ProcessTimer;
2123
use crate::tracing::time;
@@ -46,14 +48,24 @@ pub struct RunTreeArgs {
4648
/// Name of the main module (the module run as __main__)
4749
#[arg(long = "main-module")]
4850
main_module: Option<String>,
51+
52+
/// Python version to use for parsing
53+
#[arg(long = "python-version", default_value = DEFAULT_PYTHON_VERSION)]
54+
python_version: String,
4955
}
5056

5157
pub fn run(args: RunTreeArgs) -> Result<()> {
5258
let timer = ProcessTimer::new();
5359
let cwd = std::env::current_dir()?;
5460

61+
let python_version = parse_python_version(&args.python_version)?;
62+
5563
let (build_map, _) = time("Discovering sources", || {
56-
build_source_db(&args.input_dir, args.site_packages.as_deref())
64+
build_source_db(
65+
&args.input_dir,
66+
args.site_packages.as_deref(),
67+
python_version,
68+
)
5769
})?;
5870
println!("Found {} Python files", build_map.len());
5971

@@ -63,6 +75,7 @@ pub fn run(args: RunTreeArgs) -> Result<()> {
6375
verbose_output_path: args.verbose_output_path,
6476
sorted_output: args.sorted_output,
6577
main_module: args.main_module.map(|s| ModuleName::from_str(&s)),
78+
python_version,
6679
};
6780

6881
let lifeguard_output = process_source_map(source_map, &cwd, &options)?;

src/commands/show_effects.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,37 @@ use crate::debug::print_module_imports_map;
1717
use crate::imports::ImportGraph;
1818
use crate::module_parser;
1919
use crate::pyrefly::module_name::ModuleName;
20+
use crate::runner::DEFAULT_PYTHON_VERSION;
21+
use crate::runner::parse_python_version;
2022
use crate::source_map::ModuleProvider;
2123
use crate::test_lib::TestSources;
2224

2325
#[derive(Parser)]
2426
pub struct ShowEffectsArgs {
2527
input_file: PathBuf,
28+
29+
/// Python version to use for parsing
30+
#[arg(long = "python-version", default_value = DEFAULT_PYTHON_VERSION)]
31+
python_version: String,
2632
}
2733

2834
pub fn run(args: ShowEffectsArgs) -> Result<()> {
2935
let module_name = ModuleName::from_str("current_module");
3036
let path = args.input_file;
3137
let source = std::fs::read_to_string(&path)?;
3238

39+
let python_version = parse_python_version(&args.python_version)?;
40+
let config = AnalysisConfig::with_python_version(python_version, None);
41+
3342
let sources = TestSources::new(&[("current_module", &source)]);
34-
let config = AnalysisConfig::default();
3543
let (import_graph, exports) = ImportGraph::make_with_exports(&sources, &config);
3644

37-
// Run the analysis
3845
let typ = module_parser::file_source_type(&path).unwrap();
3946
let is_init = path
4047
.file_name()
4148
.is_some_and(|f| f == "__init__.py" || f == "__init__.pyi");
42-
let module = module_parser::parse_file(&source, typ, module_name, is_init);
49+
let module =
50+
module_parser::parse_file_with_version(&source, typ, module_name, is_init, python_version);
4351
let output = analyzer::analyze(&module, &exports, &import_graph, sources.stubs(), &config);
4452

4553
// Display output

src/config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use ruff_python_ast::Expr;
1111
use ruff_python_ast::Stmt;
1212
use ruff_python_ast::StmtIf;
1313

14+
use crate::pyrefly::sys_info::PythonVersion;
1415
use crate::pyrefly::sys_info::SysInfo;
1516
use crate::traits::SysInfoExt;
1617

@@ -64,6 +65,16 @@ impl AnalysisConfig {
6465
}
6566
}
6667

68+
pub fn with_python_version(
69+
python_version: PythonVersion,
70+
main_module: Option<ModuleName>,
71+
) -> Self {
72+
Self {
73+
sys_info: SysInfo::lg_with_version(python_version),
74+
main_module,
75+
}
76+
}
77+
6778
pub fn lg_pruned_if_branches<'a>(
6879
&'a self,
6980
x: &'a StmtIf,

src/find_sources.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ use std::path::PathBuf;
2020
use anyhow::Context;
2121
use anyhow::Result;
2222
use ruff_python_ast::Stmt;
23-
use ruff_python_parser::parse_unchecked_source;
23+
use ruff_python_parser::ParseOptions;
2424
use serde::Deserialize;
2525
use walkdir::WalkDir;
2626

27+
use crate::pyrefly::sys_info::PythonVersion;
28+
use crate::runner::to_ruff_version;
2729
use crate::source_map::RawSourceMap;
2830
use crate::source_map::SourceMap;
2931
use crate::source_map::is_python_file;
@@ -121,11 +123,22 @@ fn resolve_relative_import(level: u32, module: Option<&str>, package: &str) -> O
121123

122124
/// Extract dotted module names from import statements in Python source.
123125
/// If `package` is provided, relative imports are resolved against it.
124-
fn extract_imports(source: &str, package: Option<&str>) -> Vec<String> {
125-
let parsed = parse_unchecked_source(source, ruff_python_ast::PySourceType::Python);
126+
fn extract_imports(
127+
source: &str,
128+
package: Option<&str>,
129+
python_version: PythonVersion,
130+
) -> Vec<String> {
131+
let ruff_version = to_ruff_version(&python_version);
132+
let options =
133+
ParseOptions::from(ruff_python_ast::PySourceType::Python).with_target_version(ruff_version);
134+
let parsed = ruff_python_parser::parse_unchecked(source, options);
126135
let mut imports = Vec::new();
136+
let module = match parsed.into_syntax() {
137+
ruff_python_ast::Mod::Module(m) => m,
138+
_ => return Vec::new(),
139+
};
127140

128-
for stmt in parsed.suite() {
141+
for stmt in module.body {
129142
match stmt {
130143
Stmt::Import(import) => {
131144
for alias in &import.names {
@@ -209,6 +222,7 @@ fn load_site_packages(input_dir: &Path) -> Result<Option<PathBuf>> {
209222
pub fn build_source_db(
210223
input_dir: &Path,
211224
site_packages_override: Option<&Path>,
225+
python_version: PythonVersion,
212226
) -> Result<(BTreeMap<String, String>, usize)> {
213227
let input_dir = input_dir.canonicalize()?;
214228

@@ -284,7 +298,7 @@ pub fn build_source_db(
284298
.as_ref()
285299
.and_then(|sp| file_path.strip_prefix(sp).ok())
286300
.and_then(package_from_rel_path);
287-
let imports = extract_imports(&source, package.as_deref());
301+
let imports = extract_imports(&source, package.as_deref(), python_version);
288302
for module_name in imports {
289303
if let Some(resolved) = resolve_import(&roots, &module_name) {
290304
let resolved = match resolved.canonicalize() {

src/format.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,15 +294,30 @@ fn format_comprehensions(generators: &[ruff_python_ast::Comprehension]) -> Strin
294294

295295
#[cfg(test)]
296296
mod tests {
297+
use ruff_python_ast::ModModule;
297298
use ruff_python_ast::PySourceType;
298-
use ruff_python_parser::parse_unchecked_source;
299+
use ruff_python_parser::ParseOptions;
299300

300301
use super::*;
302+
use crate::runner::default_ruff_version;
303+
304+
fn parse_module(source: &str) -> ModModule {
305+
let options =
306+
ParseOptions::from(PySourceType::Python).with_target_version(default_ruff_version());
307+
let parsed = ruff_python_parser::parse_unchecked(source, options);
308+
match parsed.into_syntax() {
309+
ruff_python_ast::Mod::Module(m) => m,
310+
_ => panic!("Expected module"),
311+
}
312+
}
301313

302314
fn parse_expr(source: &str) -> Expr {
303-
let parsed = parse_unchecked_source(source, PySourceType::Python);
304-
let module = parsed.into_syntax();
305-
match module.body.into_iter().next().expect("empty module") {
315+
match parse_module(source)
316+
.body
317+
.into_iter()
318+
.next()
319+
.expect("empty module")
320+
{
306321
ruff_python_ast::Stmt::Expr(stmt) => *stmt.value,
307322
other => panic!("Expected expression statement, got {:?}", other),
308323
}
@@ -440,9 +455,7 @@ mod tests {
440455
#[test]
441456
fn test_await() {
442457
// Parse inside an async function to make it valid
443-
let source = "async def f():\n await x";
444-
let parsed = parse_unchecked_source(source, PySourceType::Python);
445-
let module = parsed.into_syntax();
458+
let module = parse_module("async def f():\n await x");
446459
let func = match &module.body[0] {
447460
ruff_python_ast::Stmt::FunctionDef(f) => f,
448461
other => panic!("Expected FunctionDef, got {:?}", other),

0 commit comments

Comments
 (0)