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

Skip to content

bpo-14156: Make argparse.FileType work correctly for binary file mode… #13165

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ def _get_action_name(argument):
if argument is None:
return None
elif argument.option_strings:
return '/'.join(argument.option_strings)
return '/'.join(argument.option_strings)
elif argument.metavar not in (None, SUPPRESS):
return argument.metavar
elif argument.dest not in (None, SUPPRESS):
Expand Down Expand Up @@ -1259,9 +1259,9 @@ def __call__(self, string):
# the special argument "-" means sys.std{in,out}
if string == '-':
if 'r' in self._mode:
return _sys.stdin
elif 'w' in self._mode:
return _sys.stdout
return _sys.stdin.buffer if 'b' in self._mode else _sys.stdin
elif any(c in self._mode for c in 'wax'):
return _sys.stdout.buffer if 'b' in self._mode else _sys.stdout
else:
msg = _('argument "-" with mode %r') % self._mode
raise ValueError(msg)
Expand Down
115 changes: 102 additions & 13 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Author: Steven J. Bethard <[email protected]>.

import inspect
import io
import operator
import os
import shutil
import stat
Expand All @@ -11,12 +13,27 @@
import argparse
import warnings

from io import StringIO

from test.support import os_helper
from unittest import mock
class StdIOBuffer(StringIO):
pass


class StdIOBuffer(io.TextIOWrapper):
'''Replacement for writable io.StringIO that behaves more like real file

Unlike StringIO, provides a buffer attribute that holds the underlying
binary data, allowing it to replace sys.stdout/sys.stderr in more
contexts.
'''

def __init__(self, initial_value='', newline='\n'):
initial_value = initial_value.encode('utf-8')
super().__init__(io.BufferedWriter(io.BytesIO(initial_value)),
'utf-8', newline=newline)

def getvalue(self):
self.flush()
return self.buffer.raw.getvalue().decode('utf-8')


class TestCase(unittest.TestCase):

Expand All @@ -43,11 +60,14 @@ def tearDown(self):
os.chmod(os.path.join(self.temp_dir, name), stat.S_IWRITE)
shutil.rmtree(self.temp_dir, True)

def create_readonly_file(self, filename):
def create_writable_file(self, filename):
file_path = os.path.join(self.temp_dir, filename)
with open(file_path, 'w', encoding="utf-8") as file:
file.write(filename)
os.chmod(file_path, stat.S_IREAD)
return file_path

def create_readonly_file(self, filename):
os.chmod(self.create_writable_file(filename), stat.S_IREAD)

class Sig(object):

Expand Down Expand Up @@ -97,10 +117,15 @@ def stderr_to_parser_error(parse_args, *args, **kwargs):
try:
result = parse_args(*args, **kwargs)
for key in list(vars(result)):
if getattr(result, key) is sys.stdout:
attr = getattr(result, key)
if attr is sys.stdout:
setattr(result, key, old_stdout)
if getattr(result, key) is sys.stderr:
elif attr is sys.stdout.buffer:
setattr(result, key, getattr(old_stdout, 'buffer', BIN_STDOUT_SENTINEL))
elif attr is sys.stderr:
setattr(result, key, old_stderr)
elif attr is sys.stderr.buffer:
setattr(result, key, getattr(old_stderr, 'buffer', BIN_STDERR_SENTINEL))
return result
except SystemExit as e:
code = e.code
Expand Down Expand Up @@ -1565,16 +1590,40 @@ def test_r_1_replace(self):
type = argparse.FileType('r', 1, errors='replace')
self.assertEqual("FileType('r', 1, errors='replace')", repr(type))


BIN_STDOUT_SENTINEL = object()
BIN_STDERR_SENTINEL = object()


class StdStreamComparer:
def __init__(self, attr):
self.attr = attr
# We try to use the actual stdXXX.buffer attribute as our
# marker, but but under some test environments,
# sys.stdout/err are replaced by io.StringIO which won't have .buffer,
# so we use a sentinel simply to show that the tests do the right thing
# for any buffer supporting object
self.getattr = operator.attrgetter(attr)
if attr == 'stdout.buffer':
self.backupattr = BIN_STDOUT_SENTINEL
elif attr == 'stderr.buffer':
self.backupattr = BIN_STDERR_SENTINEL
else:
self.backupattr = object() # Not equal to anything

def __eq__(self, other):
return other == getattr(sys, self.attr)
try:
return other == self.getattr(sys)
except AttributeError:
return other == self.backupattr


eq_stdin = StdStreamComparer('stdin')
eq_stdout = StdStreamComparer('stdout')
eq_stderr = StdStreamComparer('stderr')
eq_bstdin = StdStreamComparer('stdin.buffer')
eq_bstdout = StdStreamComparer('stdout.buffer')
eq_bstderr = StdStreamComparer('stderr.buffer')


class RFile(object):
seen = {}
Expand Down Expand Up @@ -1653,7 +1702,7 @@ def setUp(self):
('foo', NS(x=None, spam=RFile('foo'))),
('-x foo bar', NS(x=RFile('foo'), spam=RFile('bar'))),
('bar -x foo', NS(x=RFile('foo'), spam=RFile('bar'))),
('-x - -', NS(x=eq_stdin, spam=eq_stdin)),
('-x - -', NS(x=eq_bstdin, spam=eq_bstdin)),
]


Expand All @@ -1680,8 +1729,9 @@ class TestFileTypeW(TempDirMixin, ParserTestCase):
"""Test the FileType option/argument type for writing files"""

def setUp(self):
super(TestFileTypeW, self).setUp()
super().setUp()
self.create_readonly_file('readonly')
self.create_writable_file('writable')

argument_signatures = [
Sig('-x', type=argparse.FileType('w')),
Expand All @@ -1690,13 +1740,37 @@ def setUp(self):
failures = ['-x', '', 'readonly']
successes = [
('foo', NS(x=None, spam=WFile('foo'))),
('writable', NS(x=None, spam=WFile('writable'))),
('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))),
('bar -x foo', NS(x=WFile('foo'), spam=WFile('bar'))),
('-x - -', NS(x=eq_stdout, spam=eq_stdout)),
]

@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
"non-root user required")
class TestFileTypeX(TempDirMixin, ParserTestCase):
"""Test the FileType option/argument type for writing new files only"""

def setUp(self):
super().setUp()
self.create_readonly_file('readonly')
self.create_writable_file('writable')

argument_signatures = [
Sig('-x', type=argparse.FileType('x')),
Sig('spam', type=argparse.FileType('x')),
]
failures = ['-x', '', 'readonly', 'writable']
successes = [
('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))),
('-x - -', NS(x=eq_stdout, spam=eq_stdout)),
]


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

argument_signatures = [
Sig('-x', type=argparse.FileType('wb')),
Expand All @@ -1707,7 +1781,22 @@ class TestFileTypeWB(TempDirMixin, ParserTestCase):
('foo', NS(x=None, spam=WFile('foo'))),
('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))),
('bar -x foo', NS(x=WFile('foo'), spam=WFile('bar'))),
('-x - -', NS(x=eq_stdout, spam=eq_stdout)),
('-x - -', NS(x=eq_bstdout, spam=eq_bstdout)),
]


@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
"non-root user required")
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')),
]
successes = [
('-x foo bar', NS(x=WFile('foo'), spam=WFile('bar'))),
('-x - -', NS(x=eq_bstdout, spam=eq_bstdout)),
]


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
argparse.FileType now supports an argument of '-' in binary mode, returning
the .buffer attribute of sys.stdin/sys.stdout as appropriate. Modes
including 'x' and 'a' are treated equivalently to 'w' when argument is '-'.
Patch contributed by Josh Rosenberg