(root)/
Python-3.12.0/
Lib/
test/
test_zipapp.py
       1  """Test harness for the zipapp module."""
       2  
       3  import io
       4  import pathlib
       5  import stat
       6  import sys
       7  import tempfile
       8  import unittest
       9  import zipapp
      10  import zipfile
      11  from test.support import requires_zlib
      12  from test.support import os_helper
      13  
      14  from unittest.mock import patch
      15  
      16  class ESC[4;38;5;81mZipAppTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      17  
      18      """Test zipapp module functionality."""
      19  
      20      def setUp(self):
      21          tmpdir = tempfile.TemporaryDirectory()
      22          self.addCleanup(tmpdir.cleanup)
      23          self.tmpdir = pathlib.Path(tmpdir.name)
      24  
      25      def test_create_archive(self):
      26          # Test packing a directory.
      27          source = self.tmpdir / 'source'
      28          source.mkdir()
      29          (source / '__main__.py').touch()
      30          target = self.tmpdir / 'source.pyz'
      31          zipapp.create_archive(str(source), str(target))
      32          self.assertTrue(target.is_file())
      33  
      34      def test_create_archive_with_pathlib(self):
      35          # Test packing a directory using Path objects for source and target.
      36          source = self.tmpdir / 'source'
      37          source.mkdir()
      38          (source / '__main__.py').touch()
      39          target = self.tmpdir / 'source.pyz'
      40          zipapp.create_archive(source, target)
      41          self.assertTrue(target.is_file())
      42  
      43      def test_create_archive_with_subdirs(self):
      44          # Test packing a directory includes entries for subdirectories.
      45          source = self.tmpdir / 'source'
      46          source.mkdir()
      47          (source / '__main__.py').touch()
      48          (source / 'foo').mkdir()
      49          (source / 'bar').mkdir()
      50          (source / 'foo' / '__init__.py').touch()
      51          target = io.BytesIO()
      52          zipapp.create_archive(str(source), target)
      53          target.seek(0)
      54          with zipfile.ZipFile(target, 'r') as z:
      55              self.assertIn('foo/', z.namelist())
      56              self.assertIn('bar/', z.namelist())
      57  
      58      def test_create_sorted_archive(self):
      59          # Test that zipapps order their files by name
      60          source = self.tmpdir / 'source'
      61          source.mkdir()
      62          (source / 'zed.py').touch()
      63          (source / 'bin').mkdir()
      64          (source / 'bin' / 'qux').touch()
      65          (source / 'bin' / 'baz').touch()
      66          (source / '__main__.py').touch()
      67          target = io.BytesIO()
      68          zipapp.create_archive(str(source), target)
      69          target.seek(0)
      70          with zipfile.ZipFile(target, 'r') as zf:
      71              self.assertEqual(zf.namelist(),
      72                  ["__main__.py", "bin/", "bin/baz", "bin/qux", "zed.py"])
      73  
      74      def test_create_archive_with_filter(self):
      75          # Test packing a directory and using filter to specify
      76          # which files to include.
      77          def skip_pyc_files(path):
      78              return path.suffix != '.pyc'
      79          source = self.tmpdir / 'source'
      80          source.mkdir()
      81          (source / '__main__.py').touch()
      82          (source / 'test.py').touch()
      83          (source / 'test.pyc').touch()
      84          target = self.tmpdir / 'source.pyz'
      85  
      86          zipapp.create_archive(source, target, filter=skip_pyc_files)
      87          with zipfile.ZipFile(target, 'r') as z:
      88              self.assertIn('__main__.py', z.namelist())
      89              self.assertIn('test.py', z.namelist())
      90              self.assertNotIn('test.pyc', z.namelist())
      91  
      92      def test_create_archive_filter_exclude_dir(self):
      93          # Test packing a directory and using a filter to exclude a
      94          # subdirectory (ensures that the path supplied to include
      95          # is relative to the source location, as expected).
      96          def skip_dummy_dir(path):
      97              return path.parts[0] != 'dummy'
      98          source = self.tmpdir / 'source'
      99          source.mkdir()
     100          (source / '__main__.py').touch()
     101          (source / 'test.py').touch()
     102          (source / 'dummy').mkdir()
     103          (source / 'dummy' / 'test2.py').touch()
     104          target = self.tmpdir / 'source.pyz'
     105  
     106          zipapp.create_archive(source, target, filter=skip_dummy_dir)
     107          with zipfile.ZipFile(target, 'r') as z:
     108              self.assertEqual(len(z.namelist()), 2)
     109              self.assertIn('__main__.py', z.namelist())
     110              self.assertIn('test.py', z.namelist())
     111  
     112      def test_create_archive_default_target(self):
     113          # Test packing a directory to the default name.
     114          source = self.tmpdir / 'source'
     115          source.mkdir()
     116          (source / '__main__.py').touch()
     117          zipapp.create_archive(str(source))
     118          expected_target = self.tmpdir / 'source.pyz'
     119          self.assertTrue(expected_target.is_file())
     120  
     121      @requires_zlib()
     122      def test_create_archive_with_compression(self):
     123          # Test packing a directory into a compressed archive.
     124          source = self.tmpdir / 'source'
     125          source.mkdir()
     126          (source / '__main__.py').touch()
     127          (source / 'test.py').touch()
     128          target = self.tmpdir / 'source.pyz'
     129  
     130          zipapp.create_archive(source, target, compressed=True)
     131          with zipfile.ZipFile(target, 'r') as z:
     132              for name in ('__main__.py', 'test.py'):
     133                  self.assertEqual(z.getinfo(name).compress_type,
     134                                   zipfile.ZIP_DEFLATED)
     135  
     136      def test_no_main(self):
     137          # Test that packing a directory with no __main__.py fails.
     138          source = self.tmpdir / 'source'
     139          source.mkdir()
     140          (source / 'foo.py').touch()
     141          target = self.tmpdir / 'source.pyz'
     142          with self.assertRaises(zipapp.ZipAppError):
     143              zipapp.create_archive(str(source), str(target))
     144  
     145      def test_main_and_main_py(self):
     146          # Test that supplying a main argument with __main__.py fails.
     147          source = self.tmpdir / 'source'
     148          source.mkdir()
     149          (source / '__main__.py').touch()
     150          target = self.tmpdir / 'source.pyz'
     151          with self.assertRaises(zipapp.ZipAppError):
     152              zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
     153  
     154      def test_main_written(self):
     155          # Test that the __main__.py is written correctly.
     156          source = self.tmpdir / 'source'
     157          source.mkdir()
     158          (source / 'foo.py').touch()
     159          target = self.tmpdir / 'source.pyz'
     160          zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
     161          with zipfile.ZipFile(str(target), 'r') as z:
     162              self.assertIn('__main__.py', z.namelist())
     163              self.assertIn(b'pkg.mod.fn()', z.read('__main__.py'))
     164  
     165      def test_main_only_written_once(self):
     166          # Test that we don't write multiple __main__.py files.
     167          # The initial implementation had this bug; zip files allow
     168          # multiple entries with the same name
     169          source = self.tmpdir / 'source'
     170          source.mkdir()
     171          # Write 2 files, as the original bug wrote __main__.py
     172          # once for each file written :-(
     173          # See http://bugs.python.org/review/23491/diff/13982/Lib/zipapp.py#newcode67Lib/zipapp.py:67
     174          # (line 67)
     175          (source / 'foo.py').touch()
     176          (source / 'bar.py').touch()
     177          target = self.tmpdir / 'source.pyz'
     178          zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
     179          with zipfile.ZipFile(str(target), 'r') as z:
     180              self.assertEqual(1, z.namelist().count('__main__.py'))
     181  
     182      def test_main_validation(self):
     183          # Test that invalid values for main are rejected.
     184          source = self.tmpdir / 'source'
     185          source.mkdir()
     186          target = self.tmpdir / 'source.pyz'
     187          problems = [
     188              '', 'foo', 'foo:', ':bar', '12:bar', 'a.b.c.:d',
     189              '.a:b', 'a:b.', 'a:.b', 'a:silly name'
     190          ]
     191          for main in problems:
     192              with self.subTest(main=main):
     193                  with self.assertRaises(zipapp.ZipAppError):
     194                      zipapp.create_archive(str(source), str(target), main=main)
     195  
     196      def test_default_no_shebang(self):
     197          # Test that no shebang line is written to the target by default.
     198          source = self.tmpdir / 'source'
     199          source.mkdir()
     200          (source / '__main__.py').touch()
     201          target = self.tmpdir / 'source.pyz'
     202          zipapp.create_archive(str(source), str(target))
     203          with target.open('rb') as f:
     204              self.assertNotEqual(f.read(2), b'#!')
     205  
     206      def test_custom_interpreter(self):
     207          # Test that a shebang line with a custom interpreter is written
     208          # correctly.
     209          source = self.tmpdir / 'source'
     210          source.mkdir()
     211          (source / '__main__.py').touch()
     212          target = self.tmpdir / 'source.pyz'
     213          zipapp.create_archive(str(source), str(target), interpreter='python')
     214          with target.open('rb') as f:
     215              self.assertEqual(f.read(2), b'#!')
     216              self.assertEqual(b'python\n', f.readline())
     217  
     218      def test_pack_to_fileobj(self):
     219          # Test that we can pack to a file object.
     220          source = self.tmpdir / 'source'
     221          source.mkdir()
     222          (source / '__main__.py').touch()
     223          target = io.BytesIO()
     224          zipapp.create_archive(str(source), target, interpreter='python')
     225          self.assertTrue(target.getvalue().startswith(b'#!python\n'))
     226  
     227      def test_read_shebang(self):
     228          # Test that we can read the shebang line correctly.
     229          source = self.tmpdir / 'source'
     230          source.mkdir()
     231          (source / '__main__.py').touch()
     232          target = self.tmpdir / 'source.pyz'
     233          zipapp.create_archive(str(source), str(target), interpreter='python')
     234          self.assertEqual(zipapp.get_interpreter(str(target)), 'python')
     235  
     236      def test_read_missing_shebang(self):
     237          # Test that reading the shebang line of a file without one returns None.
     238          source = self.tmpdir / 'source'
     239          source.mkdir()
     240          (source / '__main__.py').touch()
     241          target = self.tmpdir / 'source.pyz'
     242          zipapp.create_archive(str(source), str(target))
     243          self.assertEqual(zipapp.get_interpreter(str(target)), None)
     244  
     245      def test_modify_shebang(self):
     246          # Test that we can change the shebang of a file.
     247          source = self.tmpdir / 'source'
     248          source.mkdir()
     249          (source / '__main__.py').touch()
     250          target = self.tmpdir / 'source.pyz'
     251          zipapp.create_archive(str(source), str(target), interpreter='python')
     252          new_target = self.tmpdir / 'changed.pyz'
     253          zipapp.create_archive(str(target), str(new_target), interpreter='python2.7')
     254          self.assertEqual(zipapp.get_interpreter(str(new_target)), 'python2.7')
     255  
     256      def test_write_shebang_to_fileobj(self):
     257          # Test that we can change the shebang of a file, writing the result to a
     258          # file object.
     259          source = self.tmpdir / 'source'
     260          source.mkdir()
     261          (source / '__main__.py').touch()
     262          target = self.tmpdir / 'source.pyz'
     263          zipapp.create_archive(str(source), str(target), interpreter='python')
     264          new_target = io.BytesIO()
     265          zipapp.create_archive(str(target), new_target, interpreter='python2.7')
     266          self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
     267  
     268      def test_read_from_pathobj(self):
     269          # Test that we can copy an archive using a pathlib.Path object
     270          # for the source.
     271          source = self.tmpdir / 'source'
     272          source.mkdir()
     273          (source / '__main__.py').touch()
     274          target1 = self.tmpdir / 'target1.pyz'
     275          target2 = self.tmpdir / 'target2.pyz'
     276          zipapp.create_archive(source, target1, interpreter='python')
     277          zipapp.create_archive(target1, target2, interpreter='python2.7')
     278          self.assertEqual(zipapp.get_interpreter(target2), 'python2.7')
     279  
     280      def test_read_from_fileobj(self):
     281          # Test that we can copy an archive using an open file object.
     282          source = self.tmpdir / 'source'
     283          source.mkdir()
     284          (source / '__main__.py').touch()
     285          target = self.tmpdir / 'source.pyz'
     286          temp_archive = io.BytesIO()
     287          zipapp.create_archive(str(source), temp_archive, interpreter='python')
     288          new_target = io.BytesIO()
     289          temp_archive.seek(0)
     290          zipapp.create_archive(temp_archive, new_target, interpreter='python2.7')
     291          self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
     292  
     293      def test_remove_shebang(self):
     294          # Test that we can remove the shebang from a file.
     295          source = self.tmpdir / 'source'
     296          source.mkdir()
     297          (source / '__main__.py').touch()
     298          target = self.tmpdir / 'source.pyz'
     299          zipapp.create_archive(str(source), str(target), interpreter='python')
     300          new_target = self.tmpdir / 'changed.pyz'
     301          zipapp.create_archive(str(target), str(new_target), interpreter=None)
     302          self.assertEqual(zipapp.get_interpreter(str(new_target)), None)
     303  
     304      def test_content_of_copied_archive(self):
     305          # Test that copying an archive doesn't corrupt it.
     306          source = self.tmpdir / 'source'
     307          source.mkdir()
     308          (source / '__main__.py').touch()
     309          target = io.BytesIO()
     310          zipapp.create_archive(str(source), target, interpreter='python')
     311          new_target = io.BytesIO()
     312          target.seek(0)
     313          zipapp.create_archive(target, new_target, interpreter=None)
     314          new_target.seek(0)
     315          with zipfile.ZipFile(new_target, 'r') as z:
     316              self.assertEqual(set(z.namelist()), {'__main__.py'})
     317  
     318      # (Unix only) tests that archives with shebang lines are made executable
     319      @unittest.skipIf(sys.platform == 'win32',
     320                       'Windows does not support an executable bit')
     321      @os_helper.skip_unless_working_chmod
     322      def test_shebang_is_executable(self):
     323          # Test that an archive with a shebang line is made executable.
     324          source = self.tmpdir / 'source'
     325          source.mkdir()
     326          (source / '__main__.py').touch()
     327          target = self.tmpdir / 'source.pyz'
     328          zipapp.create_archive(str(source), str(target), interpreter='python')
     329          self.assertTrue(target.stat().st_mode & stat.S_IEXEC)
     330  
     331      @unittest.skipIf(sys.platform == 'win32',
     332                       'Windows does not support an executable bit')
     333      def test_no_shebang_is_not_executable(self):
     334          # Test that an archive with no shebang line is not made executable.
     335          source = self.tmpdir / 'source'
     336          source.mkdir()
     337          (source / '__main__.py').touch()
     338          target = self.tmpdir / 'source.pyz'
     339          zipapp.create_archive(str(source), str(target), interpreter=None)
     340          self.assertFalse(target.stat().st_mode & stat.S_IEXEC)
     341  
     342  
     343  class ESC[4;38;5;81mZipAppCmdlineTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     344  
     345      """Test zipapp module command line API."""
     346  
     347      def setUp(self):
     348          tmpdir = tempfile.TemporaryDirectory()
     349          self.addCleanup(tmpdir.cleanup)
     350          self.tmpdir = pathlib.Path(tmpdir.name)
     351  
     352      def make_archive(self):
     353          # Test that an archive with no shebang line is not made executable.
     354          source = self.tmpdir / 'source'
     355          source.mkdir()
     356          (source / '__main__.py').touch()
     357          target = self.tmpdir / 'source.pyz'
     358          zipapp.create_archive(source, target)
     359          return target
     360  
     361      def test_cmdline_create(self):
     362          # Test the basic command line API.
     363          source = self.tmpdir / 'source'
     364          source.mkdir()
     365          (source / '__main__.py').touch()
     366          args = [str(source)]
     367          zipapp.main(args)
     368          target = source.with_suffix('.pyz')
     369          self.assertTrue(target.is_file())
     370  
     371      def test_cmdline_copy(self):
     372          # Test copying an archive.
     373          original = self.make_archive()
     374          target = self.tmpdir / 'target.pyz'
     375          args = [str(original), '-o', str(target)]
     376          zipapp.main(args)
     377          self.assertTrue(target.is_file())
     378  
     379      def test_cmdline_copy_inplace(self):
     380          # Test copying an archive in place fails.
     381          original = self.make_archive()
     382          target = self.tmpdir / 'target.pyz'
     383          args = [str(original), '-o', str(original)]
     384          with self.assertRaises(SystemExit) as cm:
     385              zipapp.main(args)
     386          # Program should exit with a non-zero return code.
     387          self.assertTrue(cm.exception.code)
     388  
     389      def test_cmdline_copy_change_main(self):
     390          # Test copying an archive doesn't allow changing __main__.py.
     391          original = self.make_archive()
     392          target = self.tmpdir / 'target.pyz'
     393          args = [str(original), '-o', str(target), '-m', 'foo:bar']
     394          with self.assertRaises(SystemExit) as cm:
     395              zipapp.main(args)
     396          # Program should exit with a non-zero return code.
     397          self.assertTrue(cm.exception.code)
     398  
     399      @patch('sys.stdout', new_callable=io.StringIO)
     400      def test_info_command(self, mock_stdout):
     401          # Test the output of the info command.
     402          target = self.make_archive()
     403          args = [str(target), '--info']
     404          with self.assertRaises(SystemExit) as cm:
     405              zipapp.main(args)
     406          # Program should exit with a zero return code.
     407          self.assertEqual(cm.exception.code, 0)
     408          self.assertEqual(mock_stdout.getvalue(), "Interpreter: <none>\n")
     409  
     410      def test_info_error(self):
     411          # Test the info command fails when the archive does not exist.
     412          target = self.tmpdir / 'dummy.pyz'
     413          args = [str(target), '--info']
     414          with self.assertRaises(SystemExit) as cm:
     415              zipapp.main(args)
     416          # Program should exit with a non-zero return code.
     417          self.assertTrue(cm.exception.code)
     418  
     419  
     420  if __name__ == "__main__":
     421      unittest.main()