• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import functools
2import importlib.util
3import os
4import py_compile
5import shutil
6import stat
7import subprocess
8import sys
9import tempfile
10import unittest
11
12from test import support
13from test.support import os_helper, script_helper
14
15
16def without_source_date_epoch(fxn):
17    """Runs function with SOURCE_DATE_EPOCH unset."""
18    @functools.wraps(fxn)
19    def wrapper(*args, **kwargs):
20        with os_helper.EnvironmentVarGuard() as env:
21            env.unset('SOURCE_DATE_EPOCH')
22            return fxn(*args, **kwargs)
23    return wrapper
24
25
26def with_source_date_epoch(fxn):
27    """Runs function with SOURCE_DATE_EPOCH set."""
28    @functools.wraps(fxn)
29    def wrapper(*args, **kwargs):
30        with os_helper.EnvironmentVarGuard() as env:
31            env['SOURCE_DATE_EPOCH'] = '123456789'
32            return fxn(*args, **kwargs)
33    return wrapper
34
35
36# Run tests with SOURCE_DATE_EPOCH set or unset explicitly.
37class SourceDateEpochTestMeta(type(unittest.TestCase)):
38    def __new__(mcls, name, bases, dct, *, source_date_epoch):
39        cls = super().__new__(mcls, name, bases, dct)
40
41        for attr in dir(cls):
42            if attr.startswith('test_'):
43                meth = getattr(cls, attr)
44                if source_date_epoch:
45                    wrapper = with_source_date_epoch(meth)
46                else:
47                    wrapper = without_source_date_epoch(meth)
48                setattr(cls, attr, wrapper)
49
50        return cls
51
52
53class PyCompileTestsBase:
54
55    def setUp(self):
56        self.directory = tempfile.mkdtemp(dir=os.getcwd())
57        self.source_path = os.path.join(self.directory, '_test.py')
58        self.pyc_path = self.source_path + 'c'
59        self.cache_path = importlib.util.cache_from_source(self.source_path)
60        self.cwd_drive = os.path.splitdrive(os.getcwd())[0]
61        # In these tests we compute relative paths.  When using Windows, the
62        # current working directory path and the 'self.source_path' might be
63        # on different drives.  Therefore we need to switch to the drive where
64        # the temporary source file lives.
65        drive = os.path.splitdrive(self.source_path)[0]
66        if drive:
67            os.chdir(drive)
68        with open(self.source_path, 'w') as file:
69            file.write('x = 123\n')
70
71    def tearDown(self):
72        shutil.rmtree(self.directory)
73        if self.cwd_drive:
74            os.chdir(self.cwd_drive)
75
76    def test_absolute_path(self):
77        py_compile.compile(self.source_path, self.pyc_path)
78        self.assertTrue(os.path.exists(self.pyc_path))
79        self.assertFalse(os.path.exists(self.cache_path))
80
81    def test_do_not_overwrite_symlinks(self):
82        # In the face of a cfile argument being a symlink, bail out.
83        # Issue #17222
84        try:
85            os.symlink(self.pyc_path + '.actual', self.pyc_path)
86        except (NotImplementedError, OSError):
87            self.skipTest('need to be able to create a symlink for a file')
88        else:
89            assert os.path.islink(self.pyc_path)
90            with self.assertRaises(FileExistsError):
91                py_compile.compile(self.source_path, self.pyc_path)
92
93    @unittest.skipIf(not os.path.exists(os.devnull) or os.path.isfile(os.devnull),
94                     'requires os.devnull and for it to be a non-regular file')
95    def test_do_not_overwrite_nonregular_files(self):
96        # In the face of a cfile argument being a non-regular file, bail out.
97        # Issue #17222
98        with self.assertRaises(FileExistsError):
99            py_compile.compile(self.source_path, os.devnull)
100
101    def test_cache_path(self):
102        py_compile.compile(self.source_path)
103        self.assertTrue(os.path.exists(self.cache_path))
104
105    def test_cwd(self):
106        with os_helper.change_cwd(self.directory):
107            py_compile.compile(os.path.basename(self.source_path),
108                               os.path.basename(self.pyc_path))
109        self.assertTrue(os.path.exists(self.pyc_path))
110        self.assertFalse(os.path.exists(self.cache_path))
111
112    def test_relative_path(self):
113        py_compile.compile(os.path.relpath(self.source_path),
114                           os.path.relpath(self.pyc_path))
115        self.assertTrue(os.path.exists(self.pyc_path))
116        self.assertFalse(os.path.exists(self.cache_path))
117
118    @os_helper.skip_if_dac_override
119    @unittest.skipIf(os.name == 'nt',
120                     'cannot control directory permissions on Windows')
121    @os_helper.skip_unless_working_chmod
122    def test_exceptions_propagate(self):
123        # Make sure that exceptions raised thanks to issues with writing
124        # bytecode.
125        # http://bugs.python.org/issue17244
126        mode = os.stat(self.directory)
127        os.chmod(self.directory, stat.S_IREAD)
128        try:
129            with self.assertRaises(IOError):
130                py_compile.compile(self.source_path, self.pyc_path)
131        finally:
132            os.chmod(self.directory, mode.st_mode)
133
134    def test_bad_coding(self):
135        bad_coding = os.path.join(os.path.dirname(__file__),
136                                  'tokenizedata',
137                                  'bad_coding2.py')
138        with support.captured_stderr():
139            self.assertIsNone(py_compile.compile(bad_coding, doraise=False))
140        self.assertFalse(os.path.exists(
141            importlib.util.cache_from_source(bad_coding)))
142
143    def test_source_date_epoch(self):
144        py_compile.compile(self.source_path, self.pyc_path)
145        self.assertTrue(os.path.exists(self.pyc_path))
146        self.assertFalse(os.path.exists(self.cache_path))
147        with open(self.pyc_path, 'rb') as fp:
148            flags = importlib._bootstrap_external._classify_pyc(
149                fp.read(), 'test', {})
150        if os.environ.get('SOURCE_DATE_EPOCH'):
151            expected_flags = 0b11
152        else:
153            expected_flags = 0b00
154
155        self.assertEqual(flags, expected_flags)
156
157    @unittest.skipIf(sys.flags.optimize > 0, 'test does not work with -O')
158    def test_double_dot_no_clobber(self):
159        # http://bugs.python.org/issue22966
160        # py_compile foo.bar.py -> __pycache__/foo.cpython-34.pyc
161        weird_path = os.path.join(self.directory, 'foo.bar.py')
162        cache_path = importlib.util.cache_from_source(weird_path)
163        pyc_path = weird_path + 'c'
164        head, tail = os.path.split(cache_path)
165        penultimate_tail = os.path.basename(head)
166        self.assertEqual(
167            os.path.join(penultimate_tail, tail),
168            os.path.join(
169                '__pycache__',
170                'foo.bar.{}.pyc'.format(sys.implementation.cache_tag)))
171        with open(weird_path, 'w') as file:
172            file.write('x = 123\n')
173        py_compile.compile(weird_path)
174        self.assertTrue(os.path.exists(cache_path))
175        self.assertFalse(os.path.exists(pyc_path))
176
177    def test_optimization_path(self):
178        # Specifying optimized bytecode should lead to a path reflecting that.
179        self.assertIn('opt-2', py_compile.compile(self.source_path, optimize=2))
180
181    def test_invalidation_mode(self):
182        py_compile.compile(
183            self.source_path,
184            invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH,
185        )
186        with open(self.cache_path, 'rb') as fp:
187            flags = importlib._bootstrap_external._classify_pyc(
188                fp.read(), 'test', {})
189        self.assertEqual(flags, 0b11)
190        py_compile.compile(
191            self.source_path,
192            invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH,
193        )
194        with open(self.cache_path, 'rb') as fp:
195            flags = importlib._bootstrap_external._classify_pyc(
196                fp.read(), 'test', {})
197        self.assertEqual(flags, 0b1)
198
199    def test_quiet(self):
200        bad_coding = os.path.join(os.path.dirname(__file__),
201                                  'tokenizedata',
202                                  'bad_coding2.py')
203        with support.captured_stderr() as stderr:
204            self.assertIsNone(py_compile.compile(bad_coding, doraise=False, quiet=2))
205            self.assertIsNone(py_compile.compile(bad_coding, doraise=True, quiet=2))
206            self.assertEqual(stderr.getvalue(), '')
207            with self.assertRaises(py_compile.PyCompileError):
208                py_compile.compile(bad_coding, doraise=True, quiet=1)
209
210
211class PyCompileTestsWithSourceEpoch(PyCompileTestsBase,
212                                    unittest.TestCase,
213                                    metaclass=SourceDateEpochTestMeta,
214                                    source_date_epoch=True):
215    pass
216
217
218class PyCompileTestsWithoutSourceEpoch(PyCompileTestsBase,
219                                       unittest.TestCase,
220                                       metaclass=SourceDateEpochTestMeta,
221                                       source_date_epoch=False):
222    pass
223
224
225class PyCompileCLITestCase(unittest.TestCase):
226
227    def setUp(self):
228        self.directory = tempfile.mkdtemp()
229        self.source_path = os.path.join(self.directory, '_test.py')
230        self.cache_path = importlib.util.cache_from_source(self.source_path,
231                                optimization='' if __debug__ else 1)
232        with open(self.source_path, 'w') as file:
233            file.write('x = 123\n')
234
235    def tearDown(self):
236        os_helper.rmtree(self.directory)
237
238    @support.requires_subprocess()
239    def pycompilecmd(self, *args, **kwargs):
240        # assert_python_* helpers don't return proc object. We'll just use
241        # subprocess.run() instead of spawn_python() and its friends to test
242        # stdin support of the CLI.
243        opts = '-m' if __debug__ else '-Om'
244        if args and args[0] == '-' and 'input' in kwargs:
245            return subprocess.run([sys.executable, opts, 'py_compile', '-'],
246                                  input=kwargs['input'].encode(),
247                                  capture_output=True)
248        return script_helper.assert_python_ok(opts, 'py_compile', *args, **kwargs)
249
250    def pycompilecmd_failure(self, *args):
251        return script_helper.assert_python_failure('-m', 'py_compile', *args)
252
253    def test_stdin(self):
254        self.assertFalse(os.path.exists(self.cache_path))
255        result = self.pycompilecmd('-', input=self.source_path)
256        self.assertEqual(result.returncode, 0)
257        self.assertEqual(result.stdout, b'')
258        self.assertEqual(result.stderr, b'')
259        self.assertTrue(os.path.exists(self.cache_path))
260
261    def test_with_files(self):
262        rc, stdout, stderr = self.pycompilecmd(self.source_path, self.source_path)
263        self.assertEqual(rc, 0)
264        self.assertEqual(stdout, b'')
265        self.assertEqual(stderr, b'')
266        self.assertTrue(os.path.exists(self.cache_path))
267
268    def test_bad_syntax(self):
269        bad_syntax = os.path.join(os.path.dirname(__file__),
270                                  'tokenizedata',
271                                  'badsyntax_3131.py')
272        rc, stdout, stderr = self.pycompilecmd_failure(bad_syntax)
273        self.assertEqual(rc, 1)
274        self.assertEqual(stdout, b'')
275        self.assertIn(b'SyntaxError', stderr)
276
277    def test_bad_syntax_with_quiet(self):
278        bad_syntax = os.path.join(os.path.dirname(__file__),
279                                  'tokenizedata',
280                                  'badsyntax_3131.py')
281        rc, stdout, stderr = self.pycompilecmd_failure('-q', bad_syntax)
282        self.assertEqual(rc, 1)
283        self.assertEqual(stdout, b'')
284        self.assertEqual(stderr, b'')
285
286    def test_file_not_exists(self):
287        should_not_exists = os.path.join(os.path.dirname(__file__), 'should_not_exists.py')
288        rc, stdout, stderr = self.pycompilecmd_failure(self.source_path, should_not_exists)
289        self.assertEqual(rc, 1)
290        self.assertEqual(stdout, b'')
291        self.assertIn(b'no such file or directory', stderr.lower())
292
293    def test_file_not_exists_with_quiet(self):
294        should_not_exists = os.path.join(os.path.dirname(__file__), 'should_not_exists.py')
295        rc, stdout, stderr = self.pycompilecmd_failure('-q', self.source_path, should_not_exists)
296        self.assertEqual(rc, 1)
297        self.assertEqual(stdout, b'')
298        self.assertEqual(stderr, b'')
299
300
301if __name__ == "__main__":
302    unittest.main()
303