1 import functools
2 import importlib.util
3 import os
4 import py_compile
5 import shutil
6 import stat
7 import subprocess
8 import sys
9 import tempfile
10 import unittest
11
12 from test import support
13 from test.support import os_helper, script_helper
14
15
16 def 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
26 def 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.
37 class ESC[4;38;5;81mSourceDateEpochTestMeta(ESC[4;38;5;149mtype(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
53 class ESC[4;38;5;81mPyCompileTestsBase:
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
211 class ESC[4;38;5;81mPyCompileTestsWithSourceEpoch(ESC[4;38;5;149mPyCompileTestsBase,
212 ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase,
213 metaclass=ESC[4;38;5;149mSourceDateEpochTestMeta,
214 source_date_epoch=ESC[4;38;5;149mTrue):
215 pass
216
217
218 class ESC[4;38;5;81mPyCompileTestsWithoutSourceEpoch(ESC[4;38;5;149mPyCompileTestsBase,
219 ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase,
220 metaclass=ESC[4;38;5;149mSourceDateEpochTestMeta,
221 source_date_epoch=ESC[4;38;5;149mFalse):
222 pass
223
224
225 class ESC[4;38;5;81mPyCompileCLITestCase(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
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 with open(self.source_path, 'w') as file:
232 file.write('x = 123\n')
233
234 def tearDown(self):
235 os_helper.rmtree(self.directory)
236
237 @support.requires_subprocess()
238 def pycompilecmd(self, *args, **kwargs):
239 # assert_python_* helpers don't return proc object. We'll just use
240 # subprocess.run() instead of spawn_python() and its friends to test
241 # stdin support of the CLI.
242 if args and args[0] == '-' and 'input' in kwargs:
243 return subprocess.run([sys.executable, '-m', 'py_compile', '-'],
244 input=kwargs['input'].encode(),
245 capture_output=True)
246 return script_helper.assert_python_ok('-m', 'py_compile', *args, **kwargs)
247
248 def pycompilecmd_failure(self, *args):
249 return script_helper.assert_python_failure('-m', 'py_compile', *args)
250
251 def test_stdin(self):
252 result = self.pycompilecmd('-', input=self.source_path)
253 self.assertEqual(result.returncode, 0)
254 self.assertEqual(result.stdout, b'')
255 self.assertEqual(result.stderr, b'')
256 self.assertTrue(os.path.exists(self.cache_path))
257
258 def test_with_files(self):
259 rc, stdout, stderr = self.pycompilecmd(self.source_path, self.source_path)
260 self.assertEqual(rc, 0)
261 self.assertEqual(stdout, b'')
262 self.assertEqual(stderr, b'')
263 self.assertTrue(os.path.exists(self.cache_path))
264
265 def test_bad_syntax(self):
266 bad_syntax = os.path.join(os.path.dirname(__file__),
267 'tokenizedata',
268 'badsyntax_3131.py')
269 rc, stdout, stderr = self.pycompilecmd_failure(bad_syntax)
270 self.assertEqual(rc, 1)
271 self.assertEqual(stdout, b'')
272 self.assertIn(b'SyntaxError', stderr)
273
274 def test_bad_syntax_with_quiet(self):
275 bad_syntax = os.path.join(os.path.dirname(__file__),
276 'tokenizedata',
277 'badsyntax_3131.py')
278 rc, stdout, stderr = self.pycompilecmd_failure('-q', bad_syntax)
279 self.assertEqual(rc, 1)
280 self.assertEqual(stdout, b'')
281 self.assertEqual(stderr, b'')
282
283 def test_file_not_exists(self):
284 should_not_exists = os.path.join(os.path.dirname(__file__), 'should_not_exists.py')
285 rc, stdout, stderr = self.pycompilecmd_failure(self.source_path, should_not_exists)
286 self.assertEqual(rc, 1)
287 self.assertEqual(stdout, b'')
288 self.assertIn(b'no such file or directory', stderr.lower())
289
290 def test_file_not_exists_with_quiet(self):
291 should_not_exists = os.path.join(os.path.dirname(__file__), 'should_not_exists.py')
292 rc, stdout, stderr = self.pycompilecmd_failure('-q', self.source_path, should_not_exists)
293 self.assertEqual(rc, 1)
294 self.assertEqual(stdout, b'')
295 self.assertEqual(stderr, b'')
296
297
298 if __name__ == "__main__":
299 unittest.main()