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