11# Author: Steven J. Bethard <steven.bethard@gmail.com>.
22
33import inspect
4+ import io
5+ import operator
46import os
57import shutil
68import stat
1012import unittest
1113import argparse
1214
13- from io import StringIO
14-
1515from test .support import os_helper
1616from 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
2037class 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
5171class 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+
15481578class 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
15551600eq_stdin = StdStreamComparer ('stdin' )
15561601eq_stdout = StdStreamComparer ('stdout' )
15571602eq_stderr = StdStreamComparer ('stderr' )
1603+ eq_bstdin = StdStreamComparer ('stdin.buffer' )
1604+ eq_bstdout = StdStreamComparer ('stdout.buffer' )
1605+ eq_bstderr = StdStreamComparer ('stderr.buffer' )
1606+
15581607
15591608class 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" )
16791752class 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
0 commit comments