Skip to content

Commit d96f103

Browse files
authored
Merge pull request #180 from facelessuser/chore/typing
Add static typing
2 parents 3bf7386 + 32fa116 commit d96f103

File tree

20 files changed

+845
-521
lines changed

20 files changed

+845
-521
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
max-parallel: 4
1818
matrix:
1919
platform: [ubuntu-latest, windows-latest]
20-
python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0-beta.1]
20+
python-version: [3.6, 3.7, 3.8, 3.9, '3.10']
2121
include:
2222
- python-version: 3.6
2323
tox-env: py36
@@ -27,11 +27,8 @@ jobs:
2727
tox-env: py38
2828
- python-version: 3.9
2929
tox-env: py39
30-
- python-version: 3.10.0-beta.1
30+
- python-version: '3.10'
3131
tox-env: py310
32-
exclude:
33-
- platform: windows-latest
34-
python-version: 3.10.0-beta.1
3532

3633
env:
3734
TOXENV: ${{ matrix.tox-env }}

.pyspelling.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ matrix:
6767
context_visible_first: true
6868
delimiters:
6969
# Ignore lint (noqa) and coverage (pragma) as well as shebang (#!)
70-
- open: '^(?: *(?:noqa\b|pragma: no cover)|!)'
70+
- open: '^(?: *(?:noqa\b|pragma: no cover|type: .*?)|!)'
7171
close: '$'
7272
# Ignore Python encoding string -*- encoding stuff -*-
7373
- open: '^ *-\*-'

docs/src/markdown/about/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 8.3
4+
5+
- **NEW**: Officially support Python 3.10.
6+
- **NEW**: Provide type hints for API.
7+
- **FIX**: Gracefully handle calls with an empty pattern list.
8+
39
## 8.2
410

511
- **NEW**: Add support for `dir_fd` in glob patterns.

requirements/test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pytest
22
pytest-cov
33
coverage
4+
mypy

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def get_description():
6666
'Programming Language :: Python :: 3.7',
6767
'Programming Language :: Python :: 3.8',
6868
'Programming Language :: Python :: 3.9',
69+
'Programming Language :: Python :: 3.10',
6970
'Topic :: Software Development :: Libraries :: Python Modules'
7071
]
7172
)

tests/test_fnmatch.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,3 +708,22 @@ def test_limit_translate(self):
708708

709709
with self.assertRaises(_wcparse.PatternLimitException):
710710
fnmatch.translate('{1..11}', flags=fnmatch.BRACE, limit=10)
711+
712+
713+
class TestTypes(unittest.TestCase):
714+
"""Test basic sequences."""
715+
716+
def test_match_set(self):
717+
"""Test `set` matching."""
718+
719+
self.assertTrue(fnmatch.fnmatch('a', set(['a'])))
720+
721+
def test_match_tuple(self):
722+
"""Test `tuple` matching."""
723+
724+
self.assertTrue(fnmatch.fnmatch('a', tuple(['a'])))
725+
726+
def test_match_list(self):
727+
"""Test `list` matching."""
728+
729+
self.assertTrue(fnmatch.fnmatch('a', ['a']))

tests/test_glob.py

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import unittest
2424
import warnings
2525
import getpass
26+
from collections.abc import MutableMapping
27+
28+
PY310 = (3, 10) <= sys.version_info
2629

2730
# Below is general helper stuff that Python uses in `unittests`. As these
2831
# not meant for users, and could change without notice, include them
@@ -34,6 +37,87 @@
3437
TESTFN = "{}_{}_tmp".format(TESTFN, os.getpid())
3538

3639

40+
class EnvironmentVarGuard(MutableMapping):
41+
"""
42+
Class to help protect the environment variable properly. Can be used as a context manager.
43+
44+
Directly ripped from Python support tools so that we can actually try and test `~user` expansion.
45+
46+
Taken from: https://github.com/python/cpython/blob/main/Lib/test/support/os_helper.py.
47+
"""
48+
49+
def __init__(self):
50+
"""Initialize."""
51+
52+
self._environ = os.environ
53+
self._changed = {}
54+
55+
def __getitem__(self, envvar):
56+
"""Get item."""
57+
58+
return self._environ[envvar]
59+
60+
def __setitem__(self, envvar, value):
61+
"""Set item."""
62+
63+
# Remember the initial value on the first access
64+
if envvar not in self._changed:
65+
self._changed[envvar] = self._environ.get(envvar)
66+
self._environ[envvar] = value
67+
68+
def __delitem__(self, envvar):
69+
"""Delete item."""
70+
71+
# Remember the initial value on the first access
72+
if envvar not in self._changed:
73+
self._changed[envvar] = self._environ.get(envvar)
74+
if envvar in self._environ:
75+
del self._environ[envvar]
76+
77+
def keys(self):
78+
"""Get keys."""
79+
return self._environ.keys()
80+
81+
def __iter__(self):
82+
"""Iterate."""
83+
84+
return iter(self._environ)
85+
86+
def __len__(self):
87+
"""Get length."""
88+
return len(self._environ)
89+
90+
def set(self, envvar, value): # noqa: A003
91+
"""Set variable."""
92+
93+
self[envvar] = value
94+
95+
def unset(self, envvar):
96+
"""Unset variable."""
97+
98+
del self[envvar]
99+
100+
def copy(self):
101+
"""Copy environment."""
102+
103+
# We do what `os.environ.copy()` does.
104+
return dict(self)
105+
106+
def __enter__(self):
107+
"""Enter."""
108+
return self
109+
110+
def __exit__(self, *ignore_exc):
111+
"""Exit."""
112+
for (k, v) in self._changed.items():
113+
if v is None:
114+
if k in self._environ:
115+
del self._environ[k]
116+
else:
117+
self._environ[k] = v
118+
os.environ = self._environ
119+
120+
37121
@contextlib.contextmanager
38122
def change_cwd(path, quiet=False):
39123
"""
@@ -1543,19 +1627,34 @@ def test_tilde_bytes(self):
15431627
def test_tilde_user(self):
15441628
"""Test tilde user cases."""
15451629

1546-
# Accommodate non-Windows user behavior
1547-
user = None
1548-
if not sys.platform.startswith('win'):
1549-
try:
1550-
user = getpass.getuser()
1551-
except ModuleNotFoundError:
1552-
pass
1630+
if sys.platform.startswith('win') and PY310:
1631+
# In CI, and maybe on other systems, we cannot be sure we'll be able to get the user.
1632+
# So fake it by using our own user name with the current `USERPROFILE` path.
1633+
with EnvironmentVarGuard() as env:
1634+
userpath = os.environ.get('USERPROFILE')
1635+
env.clear()
1636+
user = 'test_user'
1637+
env['USERPROFILE'] = userpath
1638+
env['USERNAME'] = 'test_user'
1639+
1640+
files = os.listdir(userpath)
1641+
self.assertEqual(len(glob.glob('~{}/*'.format(user), flags=glob.T | glob.D)), len(files))
1642+
else:
1643+
# Accommodate non-Windows user behavior
1644+
user = None
1645+
if not sys.platform.startswith('win'):
1646+
try:
1647+
user = getpass.getuser()
1648+
except ModuleNotFoundError:
1649+
pass
15531650

1554-
if user is None:
1555-
user = os.path.basename(os.path.expanduser('~'))
1651+
if user is None:
1652+
# Last ditch effort to get a user.
1653+
user = os.path.basename(os.path.expanduser('~'))
15561654

1557-
files = os.listdir(os.path.expanduser('~{}'.format(user)))
1558-
self.assertEqual(len(glob.glob('~{}/*'.format(user), flags=glob.T | glob.D)), len(files))
1655+
userpath = os.path.expanduser('~{}'.format(user))
1656+
files = os.listdir(userpath)
1657+
self.assertEqual(len(glob.glob('~{}/*'.format(user), flags=glob.T | glob.D)), len(files))
15591658

15601659
def test_tilde_disabled(self):
15611660
"""Test when tilde is disabled."""
@@ -1629,3 +1728,8 @@ def test_cwd_root_dir_str_bytes(self):
16291728

16301729
with self.assertRaises(TypeError):
16311730
glob.glob('docs/*', root_dir=b'.')
1731+
1732+
def test_cwd_root_dir_empty(self):
1733+
"""Test empty patterns with current working directory."""
1734+
1735+
self.assertEqual(glob.glob([], root_dir='.'), [])

tests/test_globmatch.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,12 @@ def test_unfinished_ext(self):
996996
self.assertTrue(glob.globmatch(x + '(a|B', x + '(a|B', flags=flags))
997997
self.assertFalse(glob.globmatch(x + '(a|B', 'B', flags=flags))
998998

999+
def test_empty_pattern_lists(self):
1000+
"""Test empty pattern lists."""
1001+
1002+
self.assertFalse(glob.globmatch('test', []))
1003+
self.assertEqual(glob.globfilter(['test'], []), [])
1004+
9991005
def test_windows_drives(self):
10001006
"""Test windows drives."""
10011007

tests/test_wcparse.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,38 +45,38 @@ def test_preprocessor_sequence(self):
4545
def test_compile_expansion_okay(self):
4646
"""Test expansion is okay."""
4747

48-
self.assertEqual(len(_wcparse.compile('{1..10}', _wcparse.BRACE)), 10)
48+
self.assertEqual(len(_wcparse.compile(['{1..10}'], _wcparse.BRACE)), 10)
4949

5050
def test_compile_unique_optimization_okay(self):
5151
"""Test that redundant patterns are reduced in compile."""
5252

53-
self.assertEqual(len(_wcparse.compile('|'.join(['a'] * 10), _wcparse.SPLIT, 10)), 1)
53+
self.assertEqual(len(_wcparse.compile(['|'.join(['a'] * 10)], _wcparse.SPLIT, 10)), 1)
5454

5555
def test_translate_expansion_okay(self):
5656
"""Test expansion is okay."""
5757

58-
p1, p2 = _wcparse.translate('{1..10}', _wcparse.BRACE, 10)
58+
p1, p2 = _wcparse.translate(['{1..10}'], _wcparse.BRACE, 10)
5959
count = len(p1) + len(p2)
6060
self.assertEqual(count, 10)
6161

6262
def test_translate_unique_optimization_okay(self):
6363
"""Test that redundant patterns are reduced in translate."""
64-
p1, p2 = _wcparse.translate('|'.join(['a'] * 10), _wcparse.SPLIT, 10)
64+
p1, p2 = _wcparse.translate(['|'.join(['a'] * 10)], _wcparse.SPLIT, 10)
6565
count = len(p1) + len(p2)
6666
self.assertEqual(count, 1)
6767

6868
def test_expansion_limt(self):
6969
"""Test expansion limit."""
7070

7171
with self.assertRaises(_wcparse.PatternLimitException):
72-
_wcparse.compile('{1..11}', _wcparse.BRACE, 10)
72+
_wcparse.compile(['{1..11}'], _wcparse.BRACE, 10)
7373

7474
with self.assertRaises(_wcparse.PatternLimitException):
75-
_wcparse.compile('|'.join(['a'] * 11), _wcparse.SPLIT, 10)
75+
_wcparse.compile(['|'.join(['a'] * 11)], _wcparse.SPLIT, 10)
7676

7777
with self.assertRaises(_wcparse.PatternLimitException):
7878
_wcparse.compile(
79-
'{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5)),
79+
['{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5))],
8080
_wcparse.SPLIT | _wcparse.BRACE, 10
8181
)
8282

@@ -90,14 +90,14 @@ def test_expansion_limt_translation(self):
9090
"""Test expansion limit."""
9191

9292
with self.assertRaises(_wcparse.PatternLimitException):
93-
_wcparse.translate('{1..11}', _wcparse.BRACE, 10)
93+
_wcparse.translate(['{1..11}'], _wcparse.BRACE, 10)
9494

9595
with self.assertRaises(_wcparse.PatternLimitException):
96-
_wcparse.translate('|'.join(['a'] * 11), _wcparse.SPLIT, 10)
96+
_wcparse.translate(['|'.join(['a'] * 11)], _wcparse.SPLIT, 10)
9797

9898
with self.assertRaises(_wcparse.PatternLimitException):
9999
_wcparse.translate(
100-
'{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5)),
100+
['{{{},{}}}'.format('|'.join(['a'] * 6), '|'.join(['a'] * 5))],
101101
_wcparse.SPLIT | _wcparse.BRACE, 10
102102
)
103103

@@ -110,12 +110,12 @@ def test_expansion_limt_translation(self):
110110
def test_expansion_no_limit_compile(self):
111111
"""Test no expansion limit compile."""
112112

113-
self.assertEqual(len(_wcparse.compile('{1..11}', _wcparse.BRACE, -1)), 11)
113+
self.assertEqual(len(_wcparse.compile(['{1..11}'], _wcparse.BRACE, -1)), 11)
114114

115115
def test_expansion_no_limit_translate(self):
116116
"""Test no expansion limit translate."""
117117

118-
p1, p2 = _wcparse.translate('{1..11}', _wcparse.BRACE, 0)
118+
p1, p2 = _wcparse.translate(['{1..11}'], _wcparse.BRACE, 0)
119119
count = len(p1) + len(p2)
120120
self.assertEqual(count, 11)
121121

@@ -158,3 +158,9 @@ def test_unc_pattern(self):
158158
_wcparse.RE_WIN_DRIVE[0].match('//?/GLOBAL/UNC/server/mount/temp').group(0),
159159
'//?/GLOBAL/UNC/server/mount/'
160160
)
161+
162+
def test_bad_root_dir(self):
163+
"""Test bad root directory."""
164+
165+
with self.assertRaises(TypeError):
166+
_wcparse.compile(['string'], _wcparse.PATHNAME | _wcparse.REALPATH, 0).match('string', b'rdir', None)

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ deps=
1010
-r requirements/setup.txt
1111
-r requirements/test.txt
1212
commands=
13+
{envbindir}/mypy --strict --show-error-codes --no-warn-unused-ignores wcmatch
1314
{envbindir}/py.test --cov wcmatch --cov-append tests
1415
{envbindir}/coverage html -d {envtmpdir}/coverage
1516
{envbindir}/coverage xml

0 commit comments

Comments
 (0)