1
1
# Author: Steven J. Bethard <[email protected] >.
2
2
3
3
import inspect
4
+ import io
5
+ import operator
4
6
import os
5
7
import shutil
6
8
import stat
10
12
import unittest
11
13
import argparse
12
14
13
- from io import StringIO
14
-
15
15
from test import support
16
16
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
+
19
36
20
37
class TestCase (unittest .TestCase ):
21
38
@@ -42,11 +59,14 @@ def tearDown(self):
42
59
os .chmod (os .path .join (self .temp_dir , name ), stat .S_IWRITE )
43
60
shutil .rmtree (self .temp_dir , True )
44
61
45
- def create_readonly_file (self , filename ):
62
+ def create_writable_file (self , filename ):
46
63
file_path = os .path .join (self .temp_dir , filename )
47
64
with open (file_path , 'w' ) as file :
48
65
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 )
50
70
51
71
class Sig (object ):
52
72
@@ -96,10 +116,15 @@ def stderr_to_parser_error(parse_args, *args, **kwargs):
96
116
try :
97
117
result = parse_args (* args , ** kwargs )
98
118
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 :
100
121
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 :
102
125
setattr (result , key , old_stderr )
126
+ elif attr is sys .stderr .buffer :
127
+ setattr (result , key , getattr (old_stderr , 'buffer' , BIN_STDERR_SENTINEL ))
103
128
return result
104
129
except SystemExit as e :
105
130
code = e .code
@@ -1545,16 +1570,40 @@ def test_r_1_replace(self):
1545
1570
type = argparse .FileType ('r' , 1 , errors = 'replace' )
1546
1571
self .assertEqual ("FileType('r', 1, errors='replace')" , repr (type ))
1547
1572
1573
+
1574
+ BIN_STDOUT_SENTINEL = object ()
1575
+ BIN_STDERR_SENTINEL = object ()
1576
+
1577
+
1548
1578
class StdStreamComparer :
1549
1579
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
1551
1592
1552
1593
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
+
1554
1599
1555
1600
eq_stdin = StdStreamComparer ('stdin' )
1556
1601
eq_stdout = StdStreamComparer ('stdout' )
1557
1602
eq_stderr = StdStreamComparer ('stderr' )
1603
+ eq_bstdin = StdStreamComparer ('stdin.buffer' )
1604
+ eq_bstdout = StdStreamComparer ('stdout.buffer' )
1605
+ eq_bstderr = StdStreamComparer ('stderr.buffer' )
1606
+
1558
1607
1559
1608
class RFile (object ):
1560
1609
seen = {}
@@ -1631,7 +1680,7 @@ def setUp(self):
1631
1680
('foo' , NS (x = None , spam = RFile ('foo' ))),
1632
1681
('-x foo bar' , NS (x = RFile ('foo' ), spam = RFile ('bar' ))),
1633
1682
('bar -x foo' , NS (x = RFile ('foo' ), spam = RFile ('bar' ))),
1634
- ('-x - -' , NS (x = eq_stdin , spam = eq_stdin )),
1683
+ ('-x - -' , NS (x = eq_bstdin , spam = eq_bstdin )),
1635
1684
]
1636
1685
1637
1686
@@ -1658,8 +1707,9 @@ class TestFileTypeW(TempDirMixin, ParserTestCase):
1658
1707
"""Test the FileType option/argument type for writing files"""
1659
1708
1660
1709
def setUp (self ):
1661
- super (TestFileTypeW , self ).setUp ()
1710
+ super ().setUp ()
1662
1711
self .create_readonly_file ('readonly' )
1712
+ self .create_writable_file ('writable' )
1663
1713
1664
1714
argument_signatures = [
1665
1715
Sig ('-x' , type = argparse .FileType ('w' )),
@@ -1668,13 +1718,37 @@ def setUp(self):
1668
1718
failures = ['-x' , '' , 'readonly' ]
1669
1719
successes = [
1670
1720
('foo' , NS (x = None , spam = WFile ('foo' ))),
1721
+ ('writable' , NS (x = None , spam = WFile ('writable' ))),
1671
1722
('-x foo bar' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
1672
1723
('bar -x foo' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
1673
1724
('-x - -' , NS (x = eq_stdout , spam = eq_stdout )),
1674
1725
]
1675
1726
1727
+ @unittest .skipIf (hasattr (os , 'geteuid' ) and os .geteuid () == 0 ,
1728
+ "non-root user required" )
1729
+ class TestFileTypeX (TempDirMixin , ParserTestCase ):
1730
+ """Test the FileType option/argument type for writing new files only"""
1731
+
1732
+ def setUp (self ):
1733
+ super ().setUp ()
1734
+ self .create_readonly_file ('readonly' )
1735
+ self .create_writable_file ('writable' )
1736
+
1737
+ argument_signatures = [
1738
+ Sig ('-x' , type = argparse .FileType ('x' )),
1739
+ Sig ('spam' , type = argparse .FileType ('x' )),
1740
+ ]
1741
+ failures = ['-x' , '' , 'readonly' , 'writable' ]
1742
+ successes = [
1743
+ ('-x foo bar' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
1744
+ ('-x - -' , NS (x = eq_stdout , spam = eq_stdout )),
1745
+ ]
1746
+
1676
1747
1748
+ @unittest .skipIf (hasattr (os , 'geteuid' ) and os .geteuid () == 0 ,
1749
+ "non-root user required" )
1677
1750
class TestFileTypeWB (TempDirMixin , ParserTestCase ):
1751
+ """Test the FileType option/argument type for writing binary files"""
1678
1752
1679
1753
argument_signatures = [
1680
1754
Sig ('-x' , type = argparse .FileType ('wb' )),
@@ -1685,7 +1759,22 @@ class TestFileTypeWB(TempDirMixin, ParserTestCase):
1685
1759
('foo' , NS (x = None , spam = WFile ('foo' ))),
1686
1760
('-x foo bar' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
1687
1761
('bar -x foo' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
1688
- ('-x - -' , NS (x = eq_stdout , spam = eq_stdout )),
1762
+ ('-x - -' , NS (x = eq_bstdout , spam = eq_bstdout )),
1763
+ ]
1764
+
1765
+
1766
+ @unittest .skipIf (hasattr (os , 'geteuid' ) and os .geteuid () == 0 ,
1767
+ "non-root user required" )
1768
+ class TestFileTypeXB (TestFileTypeX ):
1769
+ "Test the FileType option/argument type for writing new binary files only"
1770
+
1771
+ argument_signatures = [
1772
+ Sig ('-x' , type = argparse .FileType ('xb' )),
1773
+ Sig ('spam' , type = argparse .FileType ('xb' )),
1774
+ ]
1775
+ successes = [
1776
+ ('-x foo bar' , NS (x = WFile ('foo' ), spam = WFile ('bar' ))),
1777
+ ('-x - -' , NS (x = eq_bstdout , spam = eq_bstdout )),
1689
1778
]
1690
1779
1691
1780
0 commit comments