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