Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ee18df4

Browse files
bpo-14156: Make argparse.FileType work correctly for binary file modes when argument is '-' (GH-13165)
Also made modes containing 'a' or 'x' act the same as a mode containing 'w' when argument is '-' (so 'a'/'x' return sys.stdout like 'w', and 'ab'/'xb' return sys.stdout.buffer like 'wb'). (cherry picked from commit eafec26) Co-authored-by: MojoVampire <[email protected]>
1 parent 4716f70 commit ee18df4

File tree

3 files changed

+110
-17
lines changed

3 files changed

+110
-17
lines changed

Lib/argparse.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ def _get_action_name(argument):
726726
if argument is None:
727727
return None
728728
elif argument.option_strings:
729-
return '/'.join(argument.option_strings)
729+
return '/'.join(argument.option_strings)
730730
elif argument.metavar not in (None, SUPPRESS):
731731
return argument.metavar
732732
elif argument.dest not in (None, SUPPRESS):
@@ -1256,9 +1256,9 @@ def __call__(self, string):
12561256
# the special argument "-" means sys.std{in,out}
12571257
if string == '-':
12581258
if 'r' in self._mode:
1259-
return _sys.stdin
1260-
elif 'w' in self._mode:
1261-
return _sys.stdout
1259+
return _sys.stdin.buffer if 'b' in self._mode else _sys.stdin
1260+
elif any(c in self._mode for c in 'wax'):
1261+
return _sys.stdout.buffer if 'b' in self._mode else _sys.stdout
12621262
else:
12631263
msg = _('argument "-" with mode %r') % self._mode
12641264
raise ValueError(msg)

Lib/test/test_argparse.py

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Author: Steven J. Bethard <[email protected]>.
22

33
import inspect
4+
import io
5+
import operator
46
import os
57
import shutil
68
import stat
@@ -10,12 +12,27 @@
1012
import unittest
1113
import argparse
1214

13-
from io import StringIO
14-
1515
from test.support import os_helper
1616
from unittest import mock
17-
class StdIOBuffer(StringIO):
18-
pass
17+
18+
19+
class StdIOBuffer(io.TextIOWrapper):
20+
'''Replacement for writable io.StringIO that behaves more like real file
21+
22+
Unlike StringIO, provides a buffer attribute that holds the underlying
23+
binary data, allowing it to replace sys.stdout/sys.stderr in more
24+
contexts.
25+
'''
26+
27+
def __init__(self, initial_value='', newline='\n'):
28+
initial_value = initial_value.encode('utf-8')
29+
super().__init__(io.BufferedWriter(io.BytesIO(initial_value)),
30+
'utf-8', newline=newline)
31+
32+
def getvalue(self):
33+
self.flush()
34+
return self.buffer.raw.getvalue().decode('utf-8')
35+
1936

2037
class TestCase(unittest.TestCase):
2138

@@ -42,11 +59,14 @@ def tearDown(self):
4259
os.chmod(os.path.join(self.temp_dir, name), stat.S_IWRITE)
4360
shutil.rmtree(self.temp_dir, True)
4461

45-
def create_readonly_file(self, filename):
62+
def create_writable_file(self, filename):
4663
file_path = os.path.join(self.temp_dir, filename)
4764
with open(file_path, 'w', encoding="utf-8") as file:
4865
file.write(filename)
49-
os.chmod(file_path, stat.S_IREAD)
66+
return file_path
67+
68+
def create_readonly_file(self, filename):
69+
os.chmod(self.create_writable_file(filename), stat.S_IREAD)
5070

5171
class Sig(object):
5272

@@ -96,10 +116,15 @@ def stderr_to_parser_error(parse_args, *args, **kwargs):
96116
try:
97117
result = parse_args(*args, **kwargs)
98118
for key in list(vars(result)):
99-
if getattr(result, key) is sys.stdout:
119+
attr = getattr(result, key)
120+
if attr is sys.stdout:
100121
setattr(result, key, old_stdout)
101-
if getattr(result, key) is sys.stderr:
122+
elif attr is sys.stdout.buffer:
123+
setattr(result, key, getattr(old_stdout, 'buffer', BIN_STDOUT_SENTINEL))
124+
elif attr is sys.stderr:
102125
setattr(result, key, old_stderr)
126+
elif attr is sys.stderr.buffer:
127+
setattr(result, key, getattr(old_stderr, 'buffer', BIN_STDERR_SENTINEL))
103128
return result
104129
except SystemExit as e:
105130
code = e.code
@@ -1545,16 +1570,40 @@ def test_r_1_replace(self):
15451570
type = argparse.FileType('r', 1, errors='replace')
15461571
self.assertEqual("FileType('r', 1, errors='replace')", repr(type))
15471572

1573+
1574+
BIN_STDOUT_SENTINEL = object()
1575+
BIN_STDERR_SENTINEL = object()
1576+
1577+
15481578
class StdStreamComparer:
15491579
def __init__(self, attr):
1550-
self.attr = attr
1580+
# We try to use the actual stdXXX.buffer attribute as our
1581+
# marker, but but under some test environments,
1582+
# sys.stdout/err are replaced by io.StringIO which won't have .buffer,
1583+
# so we use a sentinel simply to show that the tests do the right thing
1584+
# for any buffer supporting object
1585+
self.getattr = operator.attrgetter(attr)
1586+
if attr == 'stdout.buffer':
1587+
self.backupattr = BIN_STDOUT_SENTINEL
1588+
elif attr == 'stderr.buffer':
1589+
self.backupattr = BIN_STDERR_SENTINEL
1590+
else:
1591+
self.backupattr = object() # Not equal to anything
15511592

15521593
def __eq__(self, other):
1553-
return other == getattr(sys, self.attr)
1594+
try:
1595+
return other == self.getattr(sys)
1596+
except AttributeError:
1597+
return other == self.backupattr
1598+
15541599

15551600
eq_stdin = StdStreamComparer('stdin')
15561601
eq_stdout = StdStreamComparer('stdout')
15571602
eq_stderr = StdStreamComparer('stderr')
1603+
eq_bstdin = StdStreamComparer('stdin.buffer')
1604+
eq_bstdout = StdStreamComparer('stdout.buffer')
1605+
eq_bstderr = StdStreamComparer('stderr.buffer')
1606+
15581607

15591608
class RFile(object):
15601609
seen = {}
@@ -1633,7 +1682,7 @@ def setUp(self):
16331682
('foo', NS(x=None, spam=RFile('foo'))),
16341683
('-x foo bar', NS(x=RFile('foo'), spam=RFile('bar'))),
16351684
('bar -x foo', NS(x=RFile('foo'), spam=RFile('bar'))),
1636-
('-x - -', NS(x=eq_stdin, spam=eq_stdin)),
1685+
('-x - -', NS(x=eq_bstdin, spam=eq_bstdin)),
16371686
]
16381687

16391688

@@ -1660,8 +1709,9 @@ class TestFileTypeW(TempDirMixin, ParserTestCase):
16601709
"""Test the FileType option/argument type for writing files"""
16611710

16621711
def setUp(self):
1663-
super(TestFileTypeW, self).setUp()
1712+
super().setUp()
16641713
self.create_readonly_file('readonly')
1714+
self.create_writable_file('writable')
16651715

16661716
argument_signatures = [
16671717
Sig('-x', type=argparse.FileType('w')),
@@ -1670,13 +1720,37 @@ def setUp(self):
16701720
failures = ['-x', '', 'readonly']
16711721
successes = [
16721722
('foo', NS(x=None, spam=WFile('foo'))),
1723+
('writable', NS(x=None, spam=WFile('writable'))),
16731724
('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))),
16741725
('bar -x foo', NS(x=WFile('foo'), spam=WFile('bar'))),
16751726
('-x - -', NS(x=eq_stdout, spam=eq_stdout)),
16761727
]
16771728

1729+
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
1730+
"non-root user required")
1731+
class TestFileTypeX(TempDirMixin, ParserTestCase):
1732+
"""Test the FileType option/argument type for writing new files only"""
1733+
1734+
def setUp(self):
1735+
super().setUp()
1736+
self.create_readonly_file('readonly')
1737+
self.create_writable_file('writable')
1738+
1739+
argument_signatures = [
1740+
Sig('-x', type=argparse.FileType('x')),
1741+
Sig('spam', type=argparse.FileType('x')),
1742+
]
1743+
failures = ['-x', '', 'readonly', 'writable']
1744+
successes = [
1745+
('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))),
1746+
('-x - -', NS(x=eq_stdout, spam=eq_stdout)),
1747+
]
1748+
16781749

1750+
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
1751+
"non-root user required")
16791752
class TestFileTypeWB(TempDirMixin, ParserTestCase):
1753+
"""Test the FileType option/argument type for writing binary files"""
16801754

16811755
argument_signatures = [
16821756
Sig('-x', type=argparse.FileType('wb')),
@@ -1687,7 +1761,22 @@ class TestFileTypeWB(TempDirMixin, ParserTestCase):
16871761
('foo', NS(x=None, spam=WFile('foo'))),
16881762
('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))),
16891763
('bar -x foo', NS(x=WFile('foo'), spam=WFile('bar'))),
1690-
('-x - -', NS(x=eq_stdout, spam=eq_stdout)),
1764+
('-x - -', NS(x=eq_bstdout, spam=eq_bstdout)),
1765+
]
1766+
1767+
1768+
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
1769+
"non-root user required")
1770+
class TestFileTypeXB(TestFileTypeX):
1771+
"Test the FileType option/argument type for writing new binary files only"
1772+
1773+
argument_signatures = [
1774+
Sig('-x', type=argparse.FileType('xb')),
1775+
Sig('spam', type=argparse.FileType('xb')),
1776+
]
1777+
successes = [
1778+
('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))),
1779+
('-x - -', NS(x=eq_bstdout, spam=eq_bstdout)),
16911780
]
16921781

16931782

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
argparse.FileType now supports an argument of '-' in binary mode, returning
2+
the .buffer attribute of sys.stdin/sys.stdout as appropriate. Modes
3+
including 'x' and 'a' are treated equivalently to 'w' when argument is '-'.
4+
Patch contributed by Josh Rosenberg

0 commit comments

Comments
 (0)