55 * LICENSE file in the root directory of this source tree.
66 */
77
8+ use std:: fs;
89use std:: path:: Path ;
910use std:: path:: PathBuf ;
1011
1112use anyhow:: Context as _;
1213use clap:: Parser ;
1314use dupe:: Dupe ;
1415use pyrefly_config:: args:: ConfigOverrideArgs ;
16+ use pyrefly_config:: environment:: uv;
1517use pyrefly_config:: resolve_unconfigured:: UnconfiguredOverride ;
1618use pyrefly_util:: absolutize:: Absolutize as _;
1719use pyrefly_util:: arc_id:: ArcId ;
@@ -20,6 +22,7 @@ use pyrefly_util::globs::FilteredGlobs;
2022use pyrefly_util:: globs:: Globs ;
2123use pyrefly_util:: globs:: HiddenDirFilter ;
2224use pyrefly_util:: includes:: Includes ;
25+ use starlark_map:: small_set:: SmallSet ;
2326use tracing:: debug;
2427use tracing:: info;
2528use 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+
340387impl 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+ }
0 commit comments