Skip to content

Commit a3d06c7

Browse files
kinto0facebook-github-bot
authored andcommitted
Add --exclude-scripts
Summary: Stack context: Check PEP 723 scripts in their own uv environments, and let `uv check` select files by path, as it does for ty. - Add `pyrefly check --exclude-scripts`. Pyrefly skips the files with a PEP 723 `# /// script` block that it finds in the project or in the given directories, and still checks files given explicitly. - `uv check` checks a project without its scripts, which must be checked with `uv check --script`. ty finds the scripts in its own walk and skips them with the same flag, so uv only passes the flag. Without it, uv must find the scripts itself and pass each one as `--project-excludes`. Differential Revision: D128114982
1 parent a425589 commit a3d06c7

2 files changed

Lines changed: 112 additions & 4 deletions

File tree

‎pyrefly/lib/commands/files.rs‎

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
use std::fs;
89
use std::path::Path;
910
use std::path::PathBuf;
1011

1112
use anyhow::Context as _;
1213
use clap::Parser;
1314
use dupe::Dupe;
1415
use pyrefly_config::args::ConfigOverrideArgs;
16+
use pyrefly_config::environment::uv;
1517
use pyrefly_config::resolve_unconfigured::UnconfiguredOverride;
1618
use pyrefly_util::absolutize::Absolutize as _;
1719
use pyrefly_util::arc_id::ArcId;
@@ -20,6 +22,7 @@ use pyrefly_util::globs::FilteredGlobs;
2022
use pyrefly_util::globs::Globs;
2123
use pyrefly_util::globs::HiddenDirFilter;
2224
use pyrefly_util::includes::Includes;
25+
use starlark_map::small_set::SmallSet;
2326
use tracing::debug;
2427
use tracing::info;
2528
use tracing::warn;
@@ -71,6 +74,10 @@ pub struct FilesArgs {
7174
/// Files to exclude when type checking.
7275
#[arg(long)]
7376
project_excludes: Option<Vec<String>>,
77+
/// Skip the PEP 723 scripts that Pyrefly finds in the project or in the given directories.
78+
/// Files given explicitly are still checked.
79+
#[arg(long)]
80+
exclude_scripts: bool,
7481

7582
/// Explicitly set the Pyrefly configuration to use when type checking.
7683
/// In "single-file checking mode," this config is applied to all files being checked, ignoring
@@ -337,6 +344,46 @@ fn get_globs_and_config_for_files(
337344
Ok((Box::new(globs), config_finder, upsell))
338345
}
339346

347+
/// Skips the PEP 723 scripts among the files of `inner`, except `explicit_files`. A script runs in
348+
/// its own environment, so a tool such as `uv check` checks scripts separately from the project.
349+
struct ExcludeScripts {
350+
inner: Box<dyn Includes>,
351+
explicit_files: SmallSet<PathBuf>,
352+
}
353+
354+
impl ExcludeScripts {
355+
fn keeps(&self, path: &Path) -> bool {
356+
self.explicit_files.contains(path)
357+
|| path.extension().is_none_or(|extension| extension != "py")
358+
|| fs::read_to_string(path)
359+
.map_or(true, |source| uv::script_metadata(&source).is_none())
360+
}
361+
}
362+
363+
impl Includes for ExcludeScripts {
364+
fn roots(&self) -> Vec<PathBuf> {
365+
self.inner.roots()
366+
}
367+
368+
fn files_iter(&self) -> anyhow::Result<Box<dyn Iterator<Item = PathBuf> + '_>> {
369+
Ok(Box::new(
370+
self.inner.files_iter()?.filter(|path| self.keeps(path)),
371+
))
372+
}
373+
374+
fn covers(&self, path: &Path) -> bool {
375+
self.inner.covers(path) && self.keeps(path)
376+
}
377+
378+
fn covers_ignoring_excludes(&self, path: &Path) -> bool {
379+
self.inner.covers_ignoring_excludes(path)
380+
}
381+
382+
fn errors(&mut self) -> Vec<anyhow::Error> {
383+
self.inner.errors()
384+
}
385+
}
386+
340387
impl FilesArgs {
341388
/// The file arguments as given on the command line. Empty means project-checking mode.
342389
pub fn files(&self) -> &[String] {
@@ -363,14 +410,20 @@ impl FilesArgs {
363410
} else {
364411
None
365412
};
366-
if self.files.is_empty() {
413+
let explicit_files = self
414+
.files
415+
.iter()
416+
.map(|file| PathBuf::from(file).absolutize())
417+
.filter(|file| file.is_file())
418+
.collect();
419+
let (includes, config_finder, upsell) = if self.files.is_empty() {
367420
get_globs_and_config_for_project(
368421
self.config,
369422
project_excludes,
370423
config_override,
371424
wrapper,
372425
scope,
373-
)
426+
)?
374427
} else {
375428
// File mode bypasses the config's globs, so `scope` is intentionally unused.
376429
get_globs_and_config_for_files(
@@ -379,8 +432,17 @@ impl FilesArgs {
379432
project_excludes,
380433
config_override,
381434
wrapper,
382-
)
383-
}
435+
)?
436+
};
437+
let includes = if self.exclude_scripts {
438+
Box::new(ExcludeScripts {
439+
inner: includes,
440+
explicit_files,
441+
})
442+
} else {
443+
includes
444+
};
445+
Ok((includes, config_finder, upsell))
384446
}
385447

386448
pub fn get(
@@ -393,7 +455,47 @@ impl FilesArgs {
393455
files,
394456
config,
395457
project_excludes: None,
458+
exclude_scripts: false,
396459
}
397460
.resolve(args, wrapper)
398461
}
399462
}
463+
464+
#[cfg(test)]
465+
mod tests {
466+
use super::*;
467+
468+
#[test]
469+
fn test_exclude_scripts() {
470+
let tempdir = tempfile::tempdir().unwrap();
471+
let root = tempdir.path();
472+
let script = root.join("script.py");
473+
let module = root.join("module.py");
474+
fs::write(&script, "# /// script\n# dependencies = [\"six\"]\n# ///\n").unwrap();
475+
fs::write(&module, "x = 1\n").unwrap();
476+
let files = |explicit_files: Vec<PathBuf>| {
477+
let includes = ExcludeScripts {
478+
inner: Box::new(FilteredGlobs::new(
479+
Globs::new(vec![root.to_string_lossy().into_owned()]).unwrap(),
480+
Globs::empty(),
481+
None,
482+
HiddenDirFilter::Disabled,
483+
)),
484+
explicit_files: explicit_files.into_iter().collect(),
485+
};
486+
let mut files = includes.files_iter().unwrap().collect::<Vec<_>>();
487+
files.sort();
488+
files
489+
};
490+
assert_eq!(
491+
files(Vec::new()),
492+
vec![module.clone()],
493+
"a script found in a directory is skipped",
494+
);
495+
assert_eq!(
496+
files(vec![script.clone()]),
497+
vec![module, script],
498+
"an explicitly given script is checked",
499+
);
500+
}
501+
}

‎website/docs/configuration.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,12 @@ interpreter. Pyrefly never creates or updates a script's environment: if the
11531153
environment does not exist, Pyrefly warns and checks the script like any other
11541154
file. Run `uv sync --script <file>` to create the environment.
11551155

1156+
To check a project without its scripts, pass `--exclude-scripts`. Pyrefly then
1157+
skips the files with a `# /// script` block that it finds in the project or in
1158+
the given directories, and still checks the files that you give explicitly.
1159+
`uv check --type-checker pyrefly` passes `--exclude-scripts`, and checks a
1160+
script with `uv check --script`.
1161+
11561162
Pyrefly runs the uv executable in the `UV` environment variable, which uv sets
11571163
for the commands that it runs, or `uv` on your `PATH`.
11581164

0 commit comments

Comments
 (0)