diff --git a/Lib/py_compile.py b/Lib/py_compile.py index 8e9dd57a5..388614e51 100644 --- a/Lib/py_compile.py +++ b/Lib/py_compile.py @@ -77,7 +77,7 @@ def _get_default_invalidation_mode(): def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1, - invalidation_mode=None): + invalidation_mode=None, quiet=0): """Byte-compile one Python source file to Python bytecode. :param file: The source file name. @@ -95,6 +95,8 @@ def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1, are -1, 0, 1 and 2. A value of -1 means to use the optimization level of the current interpreter, as given by -O command line options. :param invalidation_mode: + :param quiet: Return full output with False or 0, errors only with 1, + and no output with 2. :return: Path to the resulting byte compiled file. @@ -143,11 +145,12 @@ def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1, _optimize=optimize) except Exception as err: py_exc = PyCompileError(err.__class__, err, dfile or file) - if doraise: - raise py_exc - else: - sys.stderr.write(py_exc.msg + '\n') - return + if quiet < 2: + if doraise: + raise py_exc + else: + sys.stderr.write(py_exc.msg + '\n') + return try: dirname = os.path.dirname(cfile) if dirname: @@ -170,43 +173,40 @@ def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1, return cfile -def main(args=None): - """Compile several source files. +def main(): + import argparse - The files named in 'args' (or on the command line, if 'args' is - not specified) are compiled and the resulting bytecode is cached - in the normal manner. This function does not search a directory - structure to locate source files; it only compiles files named - explicitly. If '-' is the only parameter in args, the list of - files is taken from standard input. - - """ - if args is None: - args = sys.argv[1:] - rv = 0 - if args == ['-']: - while True: - filename = sys.stdin.readline() - if not filename: - break - filename = filename.rstrip('\n') - try: - compile(filename, doraise=True) - except PyCompileError as error: - rv = 1 - sys.stderr.write("%s\n" % error.msg) - except OSError as error: - rv = 1 - sys.stderr.write("%s\n" % error) + description = 'A simple command-line interface for py_compile module.' + parser = argparse.ArgumentParser(description=description) + parser.add_argument( + '-q', '--quiet', + action='store_true', + help='Suppress error output', + ) + parser.add_argument( + 'filenames', + nargs='+', + help='Files to compile', + ) + args = parser.parse_args() + if args.filenames == ['-']: + filenames = [filename.rstrip('\n') for filename in sys.stdin.readlines()] else: - for filename in args: - try: - compile(filename, doraise=True) - except PyCompileError as error: - # return value to indicate at least one failure - rv = 1 - sys.stderr.write("%s\n" % error.msg) - return rv + filenames = args.filenames + for filename in filenames: + try: + compile(filename, doraise=True) + except PyCompileError as error: + if args.quiet: + parser.exit(1) + else: + parser.exit(1, error.msg) + except OSError as error: + if args.quiet: + parser.exit(1) + else: + parser.exit(1, str(error)) + if __name__ == "__main__": - sys.exit(main()) + main() diff --git a/Lib/test/badsyntax_3131.py b/Lib/test/badsyntax_3131.py new file mode 100644 index 000000000..901d3744c --- /dev/null +++ b/Lib/test/badsyntax_3131.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +€ = 2 diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py index 71c7a96d6..697f16f4a 100644 --- a/Lib/test/test_py_compile.py +++ b/Lib/test/test_py_compile.py @@ -4,12 +4,13 @@ import os import py_compile import shutil import stat +import subprocess import sys import tempfile import unittest from test import support -from test.support import os_helper +from test.support import os_helper, script_helper def without_source_date_epoch(fxn): @@ -52,7 +53,7 @@ class SourceDateEpochTestMeta(type(unittest.TestCase)): class PyCompileTestsBase: def setUp(self): - self.directory = tempfile.mkdtemp() + self.directory = tempfile.mkdtemp(dir=os.getcwd()) self.source_path = os.path.join(self.directory, '_test.py') self.pyc_path = self.source_path + 'c' self.cache_path = importlib.util.cache_from_source(self.source_path) @@ -255,5 +256,77 @@ class PyCompileTestsWithoutSourceEpoch(PyCompileTestsBase, pass +class PyCompileCLITestCase(unittest.TestCase): + + def setUp(self): + self.directory = tempfile.mkdtemp() + self.source_path = os.path.join(self.directory, '_test.py') + self.cache_path = importlib.util.cache_from_source(self.source_path) + with open(self.source_path, 'w') as file: + file.write('x = 123\n') + + def tearDown(self): + os_helper.rmtree(self.directory) + + def pycompilecmd(self, *args, **kwargs): + # assert_python_* helpers don't return proc object. We'll just use + # subprocess.run() instead of spawn_python() and its friends to test + # stdin support of the CLI. + if args and args[0] == '-' and 'input' in kwargs: + return subprocess.run([sys.executable, '-m', 'py_compile', '-'], + input=kwargs['input'].encode(), + capture_output=True) + return script_helper.assert_python_ok('-m', 'py_compile', *args, **kwargs) + + def pycompilecmd_failure(self, *args): + return script_helper.assert_python_failure('-m', 'py_compile', *args) + + def test_stdin(self): + result = self.pycompilecmd('-', input=self.source_path) + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout, b'') + self.assertEqual(result.stderr, b'') + self.assertTrue(os.path.exists(self.cache_path)) + + def test_with_files(self): + rc, stdout, stderr = self.pycompilecmd(self.source_path, self.source_path) + self.assertEqual(rc, 0) + self.assertEqual(stdout, b'') + self.assertEqual(stderr, b'') + self.assertTrue(os.path.exists(self.cache_path)) + + def test_bad_syntax(self): + bad_syntax = os.path.join(os.path.dirname(__file__), 'badsyntax_3131.py') + rc, stdout, stderr = self.pycompilecmd_failure(bad_syntax) + self.assertEqual(rc, 1) + self.assertEqual(stdout, b'') + self.assertIn(b'SyntaxError', stderr) + + def test_bad_syntax_with_quiet(self): + bad_syntax = os.path.join(os.path.dirname(__file__), 'badsyntax_3131.py') + rc, stdout, stderr = self.pycompilecmd_failure('-q', bad_syntax) + self.assertEqual(rc, 1) + self.assertEqual(stdout, b'') + self.assertEqual(stderr, b'') + + def test_file_not_exists(self): + should_not_exists = os.path.join(os.path.dirname(__file__), 'should_not_exists.py') + rc, stdout, stderr = self.pycompilecmd_failure(self.source_path, should_not_exists) + self.assertEqual(rc, 1) + self.assertEqual(stdout, b'') + self.assertIn(b'no such file or directory', stderr.lower()) + + # TODO: RUSTPYTHON + if sys.platform == "win32": + test_file_not_exists = unittest.expectedFailure(test_file_not_exists) + + def test_file_not_exists_with_quiet(self): + should_not_exists = os.path.join(os.path.dirname(__file__), 'should_not_exists.py') + rc, stdout, stderr = self.pycompilecmd_failure('-q', self.source_path, should_not_exists) + self.assertEqual(rc, 1) + self.assertEqual(stdout, b'') + self.assertEqual(stderr, b'') + + if __name__ == "__main__": unittest.main()