Skip to content

Commit

Permalink
gh-58032: Deprecate the argparse.FileType type converter (GH-124664)
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiy-storchaka authored Oct 23, 2024
1 parent c75ff2e commit 834ba5a
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 45 deletions.
21 changes: 11 additions & 10 deletions Doc/deprecations/pending-removal-in-future.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +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.
* Passing the undocumented keyword argument *prefix_chars* to
:meth:`~argparse.ArgumentParser.add_argument_group` is now
deprecated.

* :mod:`array`'s ``'u'`` format code (:gh:`57281`)

* :mod:`builtins`:

* ``bool(NotImplemented)``.
Expand Down Expand Up @@ -43,6 +33,17 @@ 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.
* Passing the undocumented keyword argument *prefix_chars* to
:meth:`~argparse.ArgumentParser.add_argument_group` is now
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`.
Expand Down
25 changes: 16 additions & 9 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -865,16 +865,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='<stdin>' encoding='UTF-8'>,
outfile=<_io.TextIOWrapper name='<stdout>' encoding='UTF-8'>)
Namespace(infile=None, outfile=None)

.. index:: single: * (asterisk); in argparse module

Expand Down Expand Up @@ -1033,7 +1031,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('dest_file', type=argparse.FileType('w', encoding='latin-1'))
parser.add_argument('datapath', type=pathlib.Path)

User defined functions can be used as well:
Expand Down Expand Up @@ -1827,9 +1824,19 @@ FileType objects
>>> parser.parse_args(['-'])
Namespace(infile=<_io.TextIOWrapper name='<stdin>' 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
^^^^^^^^^^^^^^^
Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,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
Expand Down
18 changes: 13 additions & 5 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -39,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.
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
Expand Down Expand Up @@ -1252,7 +1254,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.
Expand All @@ -1269,6 +1271,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
Expand Down
57 changes: 36 additions & 21 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1773,27 +1773,43 @@ 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):
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))


Expand Down Expand Up @@ -1847,7 +1863,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"""

Expand All @@ -1860,8 +1875,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 = [
Expand All @@ -1881,7 +1896,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 = ['']
Expand All @@ -1900,8 +1915,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 = [
Expand Down Expand Up @@ -1939,8 +1954,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 = [
Expand All @@ -1962,8 +1977,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 = [
Expand All @@ -1977,8 +1992,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 = [
Expand All @@ -1994,8 +2009,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'))),
Expand All @@ -2007,7 +2022,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)),
Expand All @@ -2022,7 +2037,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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deprecate the :class:`argparse.FileType` type converter.

0 comments on commit 834ba5a

Please sign in to comment.