@@ -395,6 +395,12 @@ def make_archive(self):
395395 target = self .tmpdir / 'source.pyz'
396396 zipapp .create_archive (source , target )
397397 return target
398+
399+ def _make_tree (self , root : pathlib .Path , files : list [str ]) -> None :
400+ for rel in files :
401+ p = root / rel
402+ p .parent .mkdir (parents = True , exist_ok = True )
403+ p .touch ()
398404
399405 def test_cmdline_create (self ):
400406 # Test the basic command line API.
@@ -454,6 +460,134 @@ def test_info_error(self):
454460 # Program should exit with a non-zero return code.
455461 self .assertTrue (cm .exception .code )
456462
463+ def test_cmdline_include_then_exclude (self ):
464+ source = self .tmpdir / 'source'
465+ source .mkdir ()
466+ self ._make_tree (source , [
467+ '__main__.py' ,
468+ 'foo/a.py' ,
469+ 'foo/b.pyc' ,
470+ 'bar/c.txt' ,
471+ ])
472+
473+ # Include 'foo' (directory implies subtree), then exclude *.pyc
474+ args = [
475+ str (source ),
476+ '--include' , '*.py' ,
477+ '--include' , 'foo' ,
478+ '--exclude' , '**/*.pyc' ]
479+ zipapp .main (args )
480+
481+ target = source .with_suffix ('.pyz' )
482+ with zipfile .ZipFile (target , 'r' ) as z :
483+ names = set (z .namelist ())
484+ # Always contains __main__.py unless overridden by -m
485+ self .assertIn ('__main__.py' , names )
486+ self .assertIn ('foo/' , names )
487+ self .assertIn ('foo/a.py' , names )
488+ # Excluded by pattern
489+ self .assertNotIn ('foo/b.pyc' , names )
490+ # Not included at all since include restricted to 'foo'
491+ self .assertNotIn ('bar/' , names )
492+ self .assertNotIn ('bar/c.txt' , names )
493+
494+ def test_cmdline_multiple_includes_commas_and_extend (self ):
495+ source = self .tmpdir / 'src'
496+ source .mkdir ()
497+ self ._make_tree (source , [
498+ '__main__.py' ,
499+ 'pkg/x.py' ,
500+ 'pkg/y.txt' ,
501+ 'data/readme.txt' ,
502+ 'data/keep.bin' ,
503+ ])
504+
505+ args = [
506+ str (source ),
507+ '--include' , 'pkg,data/*.txt' ,
508+ '--include' , 'data/keep.bin' ,
509+ ]
510+ zipapp .main (args )
511+
512+ target = source .with_suffix ('.pyz' )
513+ with zipfile .ZipFile (target , 'r' ) as z :
514+ names = set (z .namelist ())
515+ # did not include root files
516+ self .assertNotIn ('__main__.py' , names )
517+ # from "pkg"
518+ self .assertIn ('pkg/' , names )
519+ self .assertIn ('pkg/x.py' , names )
520+ self .assertIn ('pkg/y.txt' , names )
521+ # from "data/*.txt"
522+ self .assertIn ('data/readme.txt' , names )
523+ # from the second --include
524+ self .assertIn ('data/keep.bin' , names )
525+
526+ def test_cmdline_exclude_directory_over_included_files (self ):
527+ source = self .tmpdir / 'tree'
528+ source .mkdir ()
529+ self ._make_tree (source , [
530+ '__main__.py' ,
531+ 'foo/a.py' ,
532+ 'foo/b.py' ,
533+ 'bar/c.py' ,
534+ ])
535+
536+ # Include all *.py, but exclude 'foo/**' entirely
537+ args = [
538+ str (source ),
539+ '--include' , '*.py' ,
540+ '--exclude' , 'foo' ,
541+ ]
542+ zipapp .main (args )
543+
544+ target = source .with_suffix ('.pyz' )
545+ with zipfile .ZipFile (target , 'r' ) as z :
546+ names = set (z .namelist ())
547+ self .assertIn ('__main__.py' , names )
548+ # foo is excluded even though files match *.py
549+ self .assertNotIn ('foo/' , names )
550+ self .assertNotIn ('foo/a.py' , names )
551+ self .assertNotIn ('foo/b.py' , names )
552+ # bar/c.py remains
553+ self .assertIn ('bar/c.py' , names )
554+
555+ def test_cmdline_normalization_and_dir_implies_subtree (self ):
556+ source = self .tmpdir / 'norm'
557+ source .mkdir ()
558+ self ._make_tree (source , [
559+ '__main__.py' ,
560+ 'a/b/c.txt' ,
561+ 'a/d/e.py' ,
562+ 'x/y/z.py' ,
563+ ])
564+
565+ # Use Windows-style backslashes and a leading './'
566+ # 'a\\b' should imply both 'a/b' and 'a/b/**'
567+ args = [
568+ str (source ),
569+ '--include' , r'.\a\b' ,
570+ '--include' , r'a\d' , # also directory → subtree
571+ '--exclude' , '**/*.py' , # exclude all *.py after include
572+ ]
573+ zipapp .main (args )
574+
575+ target = source .with_suffix ('.pyz' )
576+ with zipfile .ZipFile (target , 'r' ) as z :
577+ names = set (z .namelist ())
578+ # did not include root files
579+ self .assertNotIn ('__main__.py' , names )
580+ # from a/b subtree, c.txt should be present
581+ self .assertIn ('a/b/' , names )
582+ self .assertIn ('a/b/c.txt' , names )
583+ # from a/d subtree, e.py would match include but then be excluded by **/*.py
584+ self .assertIn ('a/d/' , names )
585+ self .assertNotIn ('a/d/e.py' , names )
586+ # x/y/z.py not included at all (not in includes)
587+ self .assertNotIn ('x/' , names )
588+ self .assertNotIn ('x/y/' , names )
589+ self .assertNotIn ('x/y/z.py' , names )
590+
457591
458592if __name__ == "__main__" :
459593 unittest .main ()
0 commit comments