11# Author: Steven J. Bethard <[email protected] >. 22
33import inspect
4+ import io
5+ import operator
46import os
57import shutil
68import stat
1113import argparse
1214import warnings
1315
14- from io import StringIO
15-
1616from test .support import os_helper
1717from unittest import mock
18- class StdIOBuffer (StringIO ):
19- pass
18+
19+
20+ class StdIOBuffer (io .TextIOWrapper ):
21+ '''Replacement for writable io.StringIO that behaves more like real file
22+
23+ Unlike StringIO, provides a buffer attribute that holds the underlying
24+ binary data, allowing it to replace sys.stdout/sys.stderr in more
25+ contexts.
26+ '''
27+
28+ def __init__ (self , initial_value = '' , newline = '\n ' ):
29+ initial_value = initial_value .encode ('utf-8' )
30+ super ().__init__ (io .BufferedWriter (io .BytesIO (initial_value )),
31+ 'utf-8' , newline = newline )
32+
33+ def getvalue (self ):
34+ self .flush ()
35+ return self .buffer .raw .getvalue ().decode ('utf-8' )
36+
2037
2138class TestCase (unittest .TestCase ):
2239
@@ -43,11 +60,14 @@ def tearDown(self):
4360 os .chmod (os .path .join (self .temp_dir , name ), stat .S_IWRITE )
4461 shutil .rmtree (self .temp_dir , True )
4562
46- def create_readonly_file (self , filename ):
63+ def create_writable_file (self , filename ):
4764 file_path = os .path .join (self .temp_dir , filename )
4865 with open (file_path , 'w' , encoding = "utf-8" ) as file :
4966 file .write (filename )
50- os .chmod (file_path , stat .S_IREAD )
67+ return file_path
68+
69+ def create_readonly_file (self , filename ):
70+ os .chmod (self .create_writable_file (filename ), stat .S_IREAD )
5171
5272class Sig (object ):
5373
@@ -97,10 +117,15 @@ def stderr_to_parser_error(parse_args, *args, **kwargs):
97117 try :
98118 result = parse_args (* args , ** kwargs )
99119 for key in list (vars (result )):
100- if getattr (result , key ) is sys .stdout :
120+ attr = getattr (result , key )
121+ if attr is sys .stdout :
101122 setattr (result , key , old_stdout )
102- if getattr (result , key ) is sys .stderr :
123+ elif attr is sys .stdout .buffer :
124+ setattr (result , key , getattr (old_stdout , 'buffer' , BIN_STDOUT_SENTINEL ))
125+ elif attr is sys .stderr :
103126 setattr (result , key , old_stderr )
127+ elif attr is sys .stderr .buffer :
128+ setattr (result , key , getattr (old_stderr , 'buffer' , BIN_STDERR_SENTINEL ))
104129 return result
105130 except SystemExit as e :
106131 code = e .code
@@ -1565,16 +1590,40 @@ def test_r_1_replace(self):
15651590 type = argparse .FileType ('r' , 1 , errors = 'replace' )
15661591 self .assertEqual ("FileType('r', 1, errors='replace')" , repr (type ))
15671592
1593+
1594+ BIN_STDOUT_SENTINEL = object ()
1595+ BIN_STDERR_SENTINEL = object ()
1596+
1597+
15681598class StdStreamComparer :
15691599 def __init__ (self , attr ):
1570- self .attr = attr
1600+ # We try to use the actual stdXXX.buffer attribute as our
1601+ # marker, but but under some test environments,
1602+ # sys.stdout/err are replaced by io.StringIO which won't have .buffer,
1603+ # so we use a sentinel simply to show that the tests do the right thing
1604+ # for any buffer supporting object
1605+ self .getattr = operator .attrgetter (attr )
1606+ if attr == 'stdout.buffer' :
1607+ self .backupattr = BIN_STDOUT_SENTINEL
1608+ elif attr == 'stderr.buffer' :
1609+ self .backupattr = BIN_STDERR_SENTINEL
1610+ else :
1611+ self .backupattr = object () # Not equal to anything
15711612
15721613 def __eq__ (self , other ):
1573- return other == getattr (sys , self .attr )
1614+ try :
1615+ return other == self .getattr (sys )
1616+ except AttributeError :
1617+ return other == self .backupattr
1618+
15741619
15751620eq_stdin = StdStreamComparer ('stdin' )
15761621eq_stdout = StdStreamComparer ('stdout' )
15771622eq_stderr = StdStreamComparer ('stderr' )
1623+ eq_bstdin = StdStreamComparer ('stdin.buffer' )
1624+ eq_bstdout = StdStreamComparer ('stdout.buffer' )
1625+ eq_bstderr = StdStreamComparer ('stderr.buffer' )
1626+
15781627
15791628class RFile (object ):
15801629 seen = {}
@@ -1653,7 +1702,7 @@ def setUp(self):
16531702 ('foo' , NS (x = None , spam = RFile ('foo' ))),
16541703 ('-x foo bar' , NS (x = RFile ('foo' ), spam = RFile ('bar' ))),
16551704 ('bar -x foo' , NS (x = RFile ('foo' ), spam = RFile ('bar' ))),
1656- ('-x - -' , NS (x = eq_stdin , spam = eq_stdin )),
1705+ ('-x - -' , NS (x = eq_bstdin , spam = eq_bstdin )),
16571706 ]
16581707
16591708
@@ -1680,8 +1729,9 @@ class TestFileTypeW(TempDirMixin, ParserTestCase):
16801729 """Test the FileType option/argument type for writing files"""
16811730
16821731 def setUp (self ):
1683- super (TestFileTypeW , self ).setUp ()
1732+ super ().setUp ()
16841733 self .create_readonly_file ('readonly' )
1734+ self .create_writable_file ('writable' )
16851735
16861736 argument_signatures = [
16871737 Sig ('-x' , type = argparse .FileType ('w' )),
@@ -1690,13 +1740,37 @@ def setUp(self):
16901740 failures = ['-x' , '' , 'readonly' ]
16911741 successes = [
16921742 ('foo' , NS (x = None , spam = WFile ('foo' ))),
1743+ ('writable' , NS (x = None , spam = WFile ('writable' ))),
16931744 ('-x foo bar' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
16941745 ('bar -x foo' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
16951746 ('-x - -' , NS (x = eq_stdout , spam = eq_stdout )),
16961747 ]
16971748
1749+ @unittest .skipIf (hasattr (os , 'geteuid' ) and os .geteuid () == 0 ,
1750+ "non-root user required" )
1751+ class TestFileTypeX (TempDirMixin , ParserTestCase ):
1752+ """Test the FileType option/argument type for writing new files only"""
1753+
1754+ def setUp (self ):
1755+ super ().setUp ()
1756+ self .create_readonly_file ('readonly' )
1757+ self .create_writable_file ('writable' )
1758+
1759+ argument_signatures = [
1760+ Sig ('-x' , type = argparse .FileType ('x' )),
1761+ Sig ('spam' , type = argparse .FileType ('x' )),
1762+ ]
1763+ failures = ['-x' , '' , 'readonly' , 'writable' ]
1764+ successes = [
1765+ ('-x foo bar' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
1766+ ('-x - -' , NS (x = eq_stdout , spam = eq_stdout )),
1767+ ]
1768+
16981769
1770+ @unittest .skipIf (hasattr (os , 'geteuid' ) and os .geteuid () == 0 ,
1771+ "non-root user required" )
16991772class TestFileTypeWB (TempDirMixin , ParserTestCase ):
1773+ """Test the FileType option/argument type for writing binary files"""
17001774
17011775 argument_signatures = [
17021776 Sig ('-x' , type = argparse .FileType ('wb' )),
@@ -1707,7 +1781,22 @@ class TestFileTypeWB(TempDirMixin, ParserTestCase):
17071781 ('foo' , NS (x = None , spam = WFile ('foo' ))),
17081782 ('-x foo bar' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
17091783 ('bar -x foo' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
1710- ('-x - -' , NS (x = eq_stdout , spam = eq_stdout )),
1784+ ('-x - -' , NS (x = eq_bstdout , spam = eq_bstdout )),
1785+ ]
1786+
1787+
1788+ @unittest .skipIf (hasattr (os , 'geteuid' ) and os .geteuid () == 0 ,
1789+ "non-root user required" )
1790+ class TestFileTypeXB (TestFileTypeX ):
1791+ "Test the FileType option/argument type for writing new binary files only"
1792+
1793+ argument_signatures = [
1794+ Sig ('-x' , type = argparse .FileType ('xb' )),
1795+ Sig ('spam' , type = argparse .FileType ('xb' )),
1796+ ]
1797+ successes = [
1798+ ('-x foo bar' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
1799+ ('-x - -' , NS (x = eq_bstdout , spam = eq_bstdout )),
17111800 ]
17121801
17131802
0 commit comments