From 8527cfa1910d852b5ea818941e30ff8f7ff13377 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 27 Sep 2024 13:10:22 +0300 Subject: [PATCH 1/7] gh-58032: Deprecate the argparse.FileType type converter --- Doc/library/argparse.rst | 34 +++++++++---------- Doc/whatsnew/3.14.rst | 6 ++++ Lib/argparse.py | 11 +++--- ...4-09-27-13-10-17.gh-issue-58032.0aNAQ0.rst | 1 + 4 files changed, 30 insertions(+), 22 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-09-27-13-10-17.gh-issue-58032.0aNAQ0.rst diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index d5a21899ae4f99..52f0460cee9c2e 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -88,7 +88,7 @@ help_ Help message for an argument metavar_ Alternate display name for the argument as shown in help nargs_ Number of times the argument can be used :class:`int`, ``'?'``, ``'*'``, or ``'+'`` required_ Indicate whether an argument is required or optional ``True`` or ``False`` -:ref:`type ` Automatically convert an argument to the given type :class:`int`, :class:`float`, ``argparse.FileType('w')``, or callable function +:ref:`type ` Automatically convert an argument to the given type :class:`int`, :class:`float`, or callable function ============================ =========================================================== ========================================================================================================================== @@ -1022,16 +1022,14 @@ See also :ref:`specifying-ambiguous-arguments`. The supported values are: output files:: >>> parser = argparse.ArgumentParser() - >>> parser.add_argument('infile', nargs='?', type=argparse.FileType('r'), - ... default=sys.stdin) - >>> parser.add_argument('outfile', nargs='?', type=argparse.FileType('w'), - ... default=sys.stdout) + >>> parser.add_argument('infile', nargs='?') + >>> parser.add_argument('outfile', nargs='?') >>> parser.parse_args(['input.txt', 'output.txt']) - Namespace(infile=<_io.TextIOWrapper name='input.txt' encoding='UTF-8'>, - outfile=<_io.TextIOWrapper name='output.txt' encoding='UTF-8'>) + Namespace(infile='input.txt', outfile='output.txt') + >>> parser.parse_args(['input.txt']) + Namespace(infile='input.txt', outfile=None) >>> parser.parse_args([]) - Namespace(infile=<_io.TextIOWrapper name='' encoding='UTF-8'>, - outfile=<_io.TextIOWrapper name='' encoding='UTF-8'>) + Namespace(infile=None, outfile=None) .. index:: single: * (asterisk); in argparse module @@ -1188,8 +1186,6 @@ Common built-in types and functions can be used as type converters: parser.add_argument('distance', type=float) parser.add_argument('street', type=ascii) parser.add_argument('code_point', type=ord) - parser.add_argument('source_file', type=open) - parser.add_argument('dest_file', type=argparse.FileType('w', encoding='latin-1')) parser.add_argument('datapath', type=pathlib.Path) User defined functions can be used as well: @@ -1218,12 +1214,6 @@ better reporting than can be given by the ``type`` keyword. A :exc:`~json.JSONDecodeError` would not be well formatted and a :exc:`FileNotFoundError` exception would not be handled at all. -Even :class:`~argparse.FileType` has its limitations for use with the ``type`` -keyword. If one argument uses *FileType* and then a subsequent argument fails, -an error is reported but the file is not automatically closed. In this case, it -would be better to wait until after the parser has run and then use the -:keyword:`with`-statement to manage the files. - For type checkers that simply check against a fixed set of values, consider using the choices_ keyword instead. @@ -2004,9 +1994,19 @@ FileType objects >>> parser.parse_args(['-']) Namespace(infile=<_io.TextIOWrapper name='' encoding='UTF-8'>) + .. note:: + + If one argument uses *FileType* and then a subsequent argument fails, + an error is reported but the file is not automatically closed. + This can also clobber the output files. + In this case, it would be better to wait until after the parser has + run and then use the :keyword:`with`-statement to manage the files. + .. versionchanged:: 3.4 Added the *encodings* and *errors* parameters. + .. deprecated:: 3.14 + Argument groups ^^^^^^^^^^^^^^^ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 6875c4c909b3c7..19c6d3c61b1920 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -385,6 +385,12 @@ Deprecated as a single positional argument. (Contributed by Serhiy Storchaka in :gh:`109218`.) +* :mod:`argparse`: + Deprecated the :class:`argparse.FileType` type converter. + Anything with resource management should be done downstream after the + arguments are parsed. + (Contributed by Serhiy Storchaka in :gh:`58032`.) + * :mod:`multiprocessing` and :mod:`concurrent.futures`: The default start method (see :ref:`multiprocessing-start-methods`) changed away from *fork* to *forkserver* on platforms where it was not already diff --git a/Lib/argparse.py b/Lib/argparse.py index 690b2a9db9481b..69ee352ab8b4a7 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -18,11 +18,12 @@ 'integers', metavar='int', nargs='+', type=int, help='an integer to be summed') parser.add_argument( - '--log', default=sys.stdout, type=argparse.FileType('w'), + '--log', help='the file where the sum should be written') args = parser.parse_args() - args.log.write('%s' % sum(args.integers)) - args.log.close() + with (open(args.log, 'w') if args.log is not None + else contextlib.nullcontext(sys.stdout)) as log: + log.write('%s' % sum(args.integers)) The module contains the following public classes: @@ -39,7 +40,7 @@ - FileType -- A factory for defining types of files to be created. As the example above shows, instances of FileType are typically passed as - the type= argument of add_argument() calls. + the type= argument of add_argument() calls. Deprecated. - Action -- The base class for parser actions. Typically actions are selected by passing strings like 'store_true' or 'append_const' to @@ -1239,7 +1240,7 @@ def __call__(self, parser, namespace, values, option_string=None): # ============== class FileType(object): - """Factory for creating file object types + """Deprecated factory for creating file object types Instances of FileType are typically passed as type= arguments to the ArgumentParser add_argument() method. diff --git a/Misc/NEWS.d/next/Library/2024-09-27-13-10-17.gh-issue-58032.0aNAQ0.rst b/Misc/NEWS.d/next/Library/2024-09-27-13-10-17.gh-issue-58032.0aNAQ0.rst new file mode 100644 index 00000000000000..278512b22a8d3f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-09-27-13-10-17.gh-issue-58032.0aNAQ0.rst @@ -0,0 +1 @@ +Deprecate the :class:`argparse.FileType` type converter. From 1eaeebf5afd76558e50564ef87fecb4cbf18f10b Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 28 Sep 2024 00:35:19 +0300 Subject: [PATCH 2/7] Update Lib/argparse.py Co-authored-by: Mariatta --- Lib/argparse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 69ee352ab8b4a7..183855d2144c3e 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -40,7 +40,7 @@ - FileType -- A factory for defining types of files to be created. As the example above shows, instances of FileType are typically passed as - the type= argument of add_argument() calls. Deprecated. + the type= argument of add_argument() calls. Deprecated since Python 3.14 - Action -- The base class for parser actions. Typically actions are selected by passing strings like 'store_true' or 'append_const' to From f00457c2e8bdcdfe2a233abcc470c8407589d49c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 28 Sep 2024 09:02:20 +0300 Subject: [PATCH 3/7] Update pending-removal-in-future.rst. --- Doc/deprecations/pending-removal-in-future.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Doc/deprecations/pending-removal-in-future.rst b/Doc/deprecations/pending-removal-in-future.rst index 3f9cf6f208221a..0232a86c9fe131 100644 --- a/Doc/deprecations/pending-removal-in-future.rst +++ b/Doc/deprecations/pending-removal-in-future.rst @@ -4,11 +4,6 @@ Pending Removal in Future Versions The following APIs will be removed in the future, although there is currently no date scheduled for their removal. -* :mod:`argparse`: Nesting argument groups and nesting mutually exclusive - groups are deprecated. - -* :mod:`array`'s ``'u'`` format code (:gh:`57281`) - * :mod:`builtins`: * ``bool(NotImplemented)``. @@ -38,6 +33,13 @@ although there is currently no date scheduled for their removal. as a single positional argument. (Contributed by Serhiy Storchaka in :gh:`109218`.) +* :mod:`argparse`: + * Nesting argument groups and nesting mutually exclusive + groups are deprecated. + * The :class:`argparse.FileType` type converter is deprecated. + +* :mod:`array`'s ``'u'`` format code (:gh:`57281`) + * :mod:`calendar`: ``calendar.January`` and ``calendar.February`` constants are deprecated and replaced by :data:`calendar.JANUARY` and :data:`calendar.FEBRUARY`. From 2353a98c41a1eac58a7e74a86201a4945c794b87 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 28 Sep 2024 09:10:44 +0300 Subject: [PATCH 4/7] Wrap long line. --- Lib/argparse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 183855d2144c3e..ae1df6d5e08438 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -40,7 +40,8 @@ - FileType -- A factory for defining types of files to be created. As the example above shows, instances of FileType are typically passed as - the type= argument of add_argument() calls. Deprecated since Python 3.14 + the type= argument of add_argument() calls. Deprecated since + Python 3.14. - Action -- The base class for parser actions. Typically actions are selected by passing strings like 'store_true' or 'append_const' to From 17ec344e3172181011664b79ac9ba35202daef05 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 28 Sep 2024 14:20:39 +0300 Subject: [PATCH 5/7] Fix Sphinx warning. --- Doc/deprecations/pending-removal-in-future.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Doc/deprecations/pending-removal-in-future.rst b/Doc/deprecations/pending-removal-in-future.rst index 0232a86c9fe131..775ec581b4740c 100644 --- a/Doc/deprecations/pending-removal-in-future.rst +++ b/Doc/deprecations/pending-removal-in-future.rst @@ -34,6 +34,7 @@ although there is currently no date scheduled for their removal. (Contributed by Serhiy Storchaka in :gh:`109218`.) * :mod:`argparse`: + * Nesting argument groups and nesting mutually exclusive groups are deprecated. * The :class:`argparse.FileType` type converter is deprecated. From 6c9660f6efa9206ad1b5322b0f0a5a6be418bae3 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 10 Oct 2024 18:59:47 +0300 Subject: [PATCH 6/7] Emit a PendingDeprecationWarning. --- Lib/argparse.py | 6 ++++ Lib/test/test_argparse.py | 58 +++++++++++++++++++++++++-------------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 9ed2186ec6595a..faf2491d4f2504 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1266,6 +1266,12 @@ class FileType(object): """ def __init__(self, mode='r', bufsize=-1, encoding=None, errors=None): + import warnings + warnings.warn( + "FileType is deprecated. Simply open files after parsing arguments.", + category=PendingDeprecationWarning, + stacklevel=2 + ) self._mode = mode self._bufsize = bufsize self._encoding = encoding diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index c9e79eb18a08fb..d9070cdc5c8eb7 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -1744,27 +1744,44 @@ def convert_arg_line_to_args(self, arg_line): # Type conversion tests # ===================== +def FileType(*args, **kwargs): + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', 'FileType is deprecated', + PendingDeprecationWarning, __name__) + return argparse.FileType(*args, **kwargs) + + +class TestFileTypeDeprecation(TestCase): + + def test(self): + parser = argparse.ArgumentParser() + with self.assertWarns(PendingDeprecationWarning) as cm: + argparse.FileType() + self.assertIn('FileType is deprecated', str(cm.warning)) + self.assertEqual(cm.filename, __file__) + + class TestFileTypeRepr(TestCase): def test_r(self): - type = argparse.FileType('r') + type = FileType('r') self.assertEqual("FileType('r')", repr(type)) def test_wb_1(self): - type = argparse.FileType('wb', 1) + type = FileType('wb', 1) self.assertEqual("FileType('wb', 1)", repr(type)) def test_r_latin(self): - type = argparse.FileType('r', encoding='latin_1') + type = FileType('r', encoding='latin_1') self.assertEqual("FileType('r', encoding='latin_1')", repr(type)) def test_w_big5_ignore(self): - type = argparse.FileType('w', encoding='big5', errors='ignore') + type = FileType('w', encoding='big5', errors='ignore') self.assertEqual("FileType('w', encoding='big5', errors='ignore')", repr(type)) def test_r_1_replace(self): - type = argparse.FileType('r', 1, errors='replace') + type = FileType('r', 1, errors='replace') self.assertEqual("FileType('r', 1, errors='replace')", repr(type)) @@ -1818,7 +1835,6 @@ def __eq__(self, other): text = text.decode('ascii') return self.name == other.name == text - class TestFileTypeR(TempDirMixin, ParserTestCase): """Test the FileType option/argument type for reading files""" @@ -1831,8 +1847,8 @@ def setUp(self): self.create_readonly_file('readonly') argument_signatures = [ - Sig('-x', type=argparse.FileType()), - Sig('spam', type=argparse.FileType('r')), + Sig('-x', type=FileType()), + Sig('spam', type=FileType('r')), ] failures = ['-x', '', 'non-existent-file.txt'] successes = [ @@ -1852,7 +1868,7 @@ def setUp(self): file.close() argument_signatures = [ - Sig('-c', type=argparse.FileType('r'), default='no-file.txt'), + Sig('-c', type=FileType('r'), default='no-file.txt'), ] # should provoke no such file error failures = [''] @@ -1871,8 +1887,8 @@ def setUp(self): file.write(file_name) argument_signatures = [ - Sig('-x', type=argparse.FileType('rb')), - Sig('spam', type=argparse.FileType('rb')), + Sig('-x', type=FileType('rb')), + Sig('spam', type=FileType('rb')), ] failures = ['-x', ''] successes = [ @@ -1910,8 +1926,8 @@ def setUp(self): self.create_writable_file('writable') argument_signatures = [ - Sig('-x', type=argparse.FileType('w')), - Sig('spam', type=argparse.FileType('w')), + Sig('-x', type=FileType('w')), + Sig('spam', type=FileType('w')), ] failures = ['-x', '', 'readonly'] successes = [ @@ -1933,8 +1949,8 @@ def setUp(self): self.create_writable_file('writable') argument_signatures = [ - Sig('-x', type=argparse.FileType('x')), - Sig('spam', type=argparse.FileType('x')), + Sig('-x', type=FileType('x')), + Sig('spam', type=FileType('x')), ] failures = ['-x', '', 'readonly', 'writable'] successes = [ @@ -1948,8 +1964,8 @@ class TestFileTypeWB(TempDirMixin, ParserTestCase): """Test the FileType option/argument type for writing binary files""" argument_signatures = [ - Sig('-x', type=argparse.FileType('wb')), - Sig('spam', type=argparse.FileType('wb')), + Sig('-x', type=FileType('wb')), + Sig('spam', type=FileType('wb')), ] failures = ['-x', ''] successes = [ @@ -1965,8 +1981,8 @@ class TestFileTypeXB(TestFileTypeX): "Test the FileType option/argument type for writing new binary files only" argument_signatures = [ - Sig('-x', type=argparse.FileType('xb')), - Sig('spam', type=argparse.FileType('xb')), + Sig('-x', type=FileType('xb')), + Sig('spam', type=FileType('xb')), ] successes = [ ('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))), @@ -1978,7 +1994,7 @@ class TestFileTypeOpenArgs(TestCase): """Test that open (the builtin) is correctly called""" def test_open_args(self): - FT = argparse.FileType + FT = FileType cases = [ (FT('rb'), ('rb', -1, None, None)), (FT('w', 1), ('w', 1, None, None)), @@ -1993,7 +2009,7 @@ def test_open_args(self): def test_invalid_file_type(self): with self.assertRaises(ValueError): - argparse.FileType('b')('-test') + FileType('b')('-test') class TestFileTypeMissingInitialization(TestCase): From 8e8e2ec4f421083ffc5029e937dc3d84538ecf45 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 11 Oct 2024 19:43:52 +0300 Subject: [PATCH 7/7] Update Lib/test/test_argparse.py --- Lib/test/test_argparse.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index d9070cdc5c8eb7..19d69d622bb27b 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -1754,7 +1754,6 @@ def FileType(*args, **kwargs): class TestFileTypeDeprecation(TestCase): def test(self): - parser = argparse.ArgumentParser() with self.assertWarns(PendingDeprecationWarning) as cm: argparse.FileType() self.assertIn('FileType is deprecated', str(cm.warning))