bpo-38731: Add --quiet option to py_compile CLI (GH-17134)

diff --git a/Doc/library/py_compile.rst b/Doc/library/py_compile.rst
index a12a5bb..9b5c8ee 100644
--- a/Doc/library/py_compile.rst
+++ b/Doc/library/py_compile.rst
@@ -125,21 +125,33 @@
       system external to Python like a build system.
 
 
-.. function:: main(args=None)
+Command-Line Interface
+----------------------
 
-   Compile several source files.  The files named in *args* (or on the command
-   line, if *args* is ``None``) are compiled and the resulting byte-code 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.
+This module can be invoked as a script to compile several source
+files.  The files named in *filenames* are compiled and the resulting
+bytecode is cached in the normal manner.  This program does not search
+a directory structure to locate source files; it only compiles files
+named explicitly. The exit status is nonzero if one of the files could
+not be compiled.
 
-   .. versionchanged:: 3.2
-      Added support for ``'-'``.
+.. program:: python -m py_compile
 
-When this module is run as a script, the :func:`main` is used to compile all the
-files named on the command line.  The exit status is nonzero if one of the files
-could not be compiled.
+.. cmdoption:: <file> ... <fileN>
+               -
+
+   Positional arguments are files to compile.  If ``-`` is the only
+   parameter, the list of files is taken from standard input.
+
+.. cmdoption:: -q, --quiet
+
+   Suppress errors output.
+
+.. versionchanged:: 3.2
+   Added support for ``-``.
+
+.. versionchanged:: 3.10
+   Added support for :option:`-q`.
 
 
 .. seealso::
diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst
index e4beb60..1865fa2 100644
--- a/Doc/whatsnew/3.10.rst
+++ b/Doc/whatsnew/3.10.rst
@@ -110,6 +110,12 @@
 :func:`~glob.iglob` which allow to specify the root directory for searching.
 (Contributed by Serhiy Storchaka in :issue:`38144`.)
 
+py_compile
+----------
+
+Added ``--quiet`` option to command-line interface of :mod:`py_compile`.
+(Contributed by Gregory Schevchenko in :issue:`38731`.)
+
 sys
 ---
 
diff --git a/Lib/py_compile.py b/Lib/py_compile.py
index 2173689..0f9b590 100644
--- a/Lib/py_compile.py
+++ b/Lib/py_compile.py
@@ -173,46 +173,40 @@
     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
-                if quiet < 2:
-                    sys.stderr.write("%s\n" % error.msg)
-            except OSError as error:
-                rv = 1
-                if quiet < 2:
-                    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 = 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
-                if quiet < 2:
-                    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/test_py_compile.py b/Lib/test/test_py_compile.py
index d8ba009..0096456 100644
--- a/Lib/test/test_py_compile.py
+++ b/Lib/test/test_py_compile.py
@@ -4,12 +4,13 @@
 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):
@@ -217,5 +218,73 @@
     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):
+        support.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)
+
+    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()
diff --git a/Misc/ACKS b/Misc/ACKS
index b585769..f5e9459 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1565,6 +1565,7 @@
 Akash Shende
 Charlie Shepherd
 Bruce Sherwood
+Gregory Shevchenko
 Alexander Shigin
 Pete Shinners
 Michael Shiplett
diff --git a/Misc/NEWS.d/next/Library/2019-11-13-07-37-11.bpo-38731.9qmcSx.rst b/Misc/NEWS.d/next/Library/2019-11-13-07-37-11.bpo-38731.9qmcSx.rst
new file mode 100644
index 0000000..ba9e522
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2019-11-13-07-37-11.bpo-38731.9qmcSx.rst
@@ -0,0 +1,2 @@
+Add ``--quiet`` option to command-line interface of :mod:`py_compile`.
+Patch by Gregory Schevchenko.