[3.9] bpo-14156: Make argparse.FileType work correctly for binary fil… · python/cpython@4d2099f (original) (raw)

1

1

`# Author: Steven J. Bethard steven.bethard@gmail.com.

`

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 +12,27 @@

`

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

``