Skip to content

Commit ea00efa

Browse files
authored
Add CI on Windows and fix tests, update path handling (#92)
1 parent a0f8888 commit ea00efa

File tree

6 files changed

+132
-127
lines changed

6 files changed

+132
-127
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ jobs:
2020
uses: epsy/python-workflows/.github/workflows/python-ci.yaml@main
2121
with:
2222
package-folder: clize
23+
python-windows-ci:
24+
name: "Run tests (Windows)"
25+
runs-on: 'windows-latest'
26+
continue-on-error: true
27+
steps:
28+
- uses: epsy/python-workflows/install-tox@main
29+
with:
30+
python-version: "3.10"
31+
- name: Test with tox
32+
uses: epsy/python-workflows/tox-ci@main
33+
with:
34+
tox-args: ""
35+
python-test-args: "-m unittest"
36+
- name: Verify that tox 'test' env ran
37+
run: cat ./tox-proof-test
38+
shell: bash
2339
mypy:
2440
name: "Run mypy on typed example"
2541
runs-on: 'ubuntu-latest'

clize/runner.py

Lines changed: 39 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# clize -- A command-line argument parser for Python
22
# Copyright (C) 2011-2016 by Yann Kaiser and contributors. See AUTHORS and
33
# COPYING for details.
4-
4+
import pathlib
55
import sys
66
import os
7+
import typing
78
from functools import partial, update_wrapper
89
import itertools
910
import shutil
@@ -289,56 +290,49 @@ def cli(self):
289290
return c
290291

291292

292-
def fix_argv(argv, path, main):
293-
"""Properly display ``python -m`` invocations"""
294-
if not path[0]:
295-
try:
296-
name = main_module_name(main)
297-
except AttributeError:
298-
pass
299-
else:
300-
argv = argv[:]
301-
argv[0] = '{0} -m {1}'.format(
302-
get_executable(sys.executable, 'python'), name)
303-
else:
304-
name = get_executable(argv[0], argv[0])
305-
argv = argv[:]
306-
argv[0] = name
307-
return argv
308-
309-
310-
def get_executable(path, default):
293+
def _get_executable(path, *, to_path=pathlib.PurePath, which=shutil.which) -> typing.Union[None, str]:
294+
"""Get the shortest invocation for a given command"""
311295
if not path:
312-
return default
313-
if path.endswith('.py'):
314-
return path
315-
basename = os.path.basename(path)
316-
try:
317-
which = shutil.which
318-
except AttributeError:
319-
which = None
320-
else:
321-
if which(basename) == path:
322-
return basename
296+
return None
297+
path = to_path(path)
298+
which_result = which(path.name)
299+
if which_result and to_path(which_result) == path:
300+
return path.name
323301
try:
324-
rel = os.path.relpath(path)
302+
rel = path.relative_to(to_path())
325303
except ValueError:
326-
return basename
327-
if rel.startswith('../'):
328-
if which is None and os.path.isabs(path):
329-
return basename
330-
return path
331-
return rel
304+
return str(path)
305+
return str(rel)
332306

333307

334308
def main_module_name(module):
335-
modname = os.path.splitext(os.path.basename(module.__file__))[0]
336-
if modname == '__main__':
337-
return module.__package__
338-
elif not module.__package__:
339-
return modname
309+
try:
310+
modname = os.path.splitext(os.path.basename(module.__file__))[0]
311+
if modname == '__main__':
312+
return module.__package__
313+
elif not module.__package__:
314+
return modname
315+
else:
316+
return module.__package__ + '.' + modname
317+
except AttributeError:
318+
return None
319+
320+
321+
def _fix_argv(argv, sys_path, main_module, *, platform=sys.platform, executable=sys.executable, get_executable=_get_executable, get_main_module_name=main_module_name):
322+
"""Tries to restore the given sys.argv to something closer to what the user would've typed"""
323+
if not sys_path[0]:
324+
name = get_main_module_name(main_module)
325+
if name is not None:
326+
argv = argv[:]
327+
argv[0] = f'{get_executable(executable) or "python"} -m {name}'
328+
elif platform.startswith("win"):
329+
argv = argv[:]
330+
argv[0] = f'{get_executable(executable) or "python"} {argv[0]}'
340331
else:
341-
return module.__package__ + '.' + modname
332+
name = get_executable(argv[0])
333+
argv = argv[:]
334+
argv[0] = name
335+
return argv
342336

343337

344338
@autokwoargs
@@ -370,7 +364,7 @@ def run(args=None, catch=(), exit=True, out=None, err=None, *fn, **kwargs):
370364
# python2.7 -m apackage
371365
# is used
372366
module = sys.modules['__main__']
373-
args = fix_argv(sys.argv, sys.path, module)
367+
args = _fix_argv(sys.argv, sys.path, module)
374368
if out is None:
375369
out = sys.stdout
376370
if err is None:

clize/tests/test_converters.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# clize -- A command-line argument parser for Python
22
# Copyright (C) 2011-2016 by Yann Kaiser and contributors. See AUTHORS and
33
# COPYING for details.
4-
4+
import unittest
55
from datetime import datetime
66
import tempfile
77
import shutil
@@ -37,13 +37,19 @@ def _test(self, conv, inp, out, *, make_signature):
3737
converters.datetime, '2014-01-01 12:00', datetime(2014, 1, 1, 12, 0))
3838

3939

40+
skip_if_windows = unittest.skipIf(sys.platform.startswith("win"), "Unsupported on Windows")
41+
42+
4043
class FileConverterTests(Tests):
4144
def setUp(self):
4245
self.temp = tempfile.mkdtemp()
4346
self.completed = False
4447

4548
def tearDown(self):
46-
shutil.rmtree(self.temp)
49+
def set_writable_and_retry(func, path, excinfo):
50+
os.chmod(path, stat.S_IWUSR)
51+
func(path)
52+
shutil.rmtree(self.temp, set_writable_and_retry)
4753

4854
def run_conv(self, conv, path):
4955
sig = support.s('*, par: c', globals={'c': conv})
@@ -175,6 +181,7 @@ def test_noperm_file_write(self):
175181
self.assertRaises(errors.BadArgumentFormat,
176182
self.run_conv, converters.file(mode='w'), path)
177183

184+
@skip_if_windows
178185
def test_noperm_dir(self):
179186
dpath = os.path.join(self.temp, 'adir')
180187
path = os.path.join(self.temp, 'adir/afile')

clize/tests/test_runner.py

Lines changed: 66 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
# Copyright (C) 2011-2016 by Yann Kaiser and contributors. See AUTHORS and
33
# COPYING for details.
44

5-
import os
5+
import pathlib
66
import sys
7-
import shutil
87
import unittest
98
from io import StringIO
109

11-
from clize.tests.util import Fixtures, Tests
10+
import repeated_test
11+
from repeated_test import options
12+
1213
from clize import runner, errors
14+
from clize.tests.util import Fixtures, Tests
1315

1416

1517
class MockModule(object):
@@ -43,107 +45,96 @@ def _test(self, filename, name, package, result):
4345

4446
def test_pudb_script(self):
4547
module = BadModule(sp+'pack/cli.py', '__main__')
46-
self.assertRaises(AttributeError, runner.main_module_name, module)
48+
self.assertEqual(None, runner.main_module_name(module))
4749

4850

49-
class GetExcecutableTests(Fixtures):
50-
def _test(self, path, default, result, which='/usr/bin'):
51-
which_backup = getattr(shutil, 'which', None)
52-
def which_(name, *args, **kwargs):
51+
@repeated_test.with_options_matrix(
52+
to_path=[pathlib.PurePosixPath, pathlib.PureWindowsPath]
53+
)
54+
class GetExecutableTests(Fixtures):
55+
def _test(self, path, expected, *, which=None, to_path):
56+
def which_(name):
5357
if which:
54-
return os.path.join('/usr/bin', name)
58+
return str(to_path(which, name))
5559
else:
5660
return None
57-
shutil.which = which_
58-
if which is None:
59-
del shutil.which
60-
try:
61-
ret = runner.get_executable(path, default)
62-
self.assertEqual(ret, result)
63-
finally:
64-
if which_backup:
65-
shutil.which = which_backup
66-
elif which is not None:
67-
del shutil.which
68-
69-
none = None, 'python', 'python'
70-
empty = '', 'myapp', 'myapp'
71-
dotpy = '/a/path/leading/to/myapp.py', None, '/a/path/leading/to/myapp.py'
72-
in_path = '/usr/bin/myapp', None, 'myapp'
73-
in_path_2 = '/usr/bin/myapp', None, 'myapp', None
74-
not_in_path = '/opt/myapp/bin/myapp', None, '/opt/myapp/bin/myapp'
75-
relpath = 'myapp/bin/myapp', None, 'myapp/bin/myapp', ''
76-
parentpath = '../myapp/bin/myapp', None, '../myapp/bin/myapp', ''
77-
parentpath_2 = '../myapp/bin/myapp', None, '../myapp/bin/myapp', None
78-
79-
def test_run_with_no_which(self):
80-
try:
81-
which_backup = shutil.which
82-
except AttributeError: # pragma: no cover
83-
return
84-
del shutil.which
85-
try:
86-
self._test(*GetExcecutableTests.empty)
87-
self.assertFalse(hasattr(shutil, 'which'))
88-
self._test(*GetExcecutableTests.in_path_2)
89-
self.assertFalse(hasattr(shutil, 'which'))
90-
finally:
91-
shutil.which = which_backup
61+
ret = runner._get_executable(path, which=which_, to_path=to_path)
62+
self.assertEqual(ret, expected)
63+
64+
none = None, None
65+
empty = '', None
66+
67+
with options(to_path=pathlib.PurePosixPath):
68+
posix_dotpy = '/a/path/leading/to/myapp.py', '/a/path/leading/to/myapp.py'
69+
posix_in_path = '/usr/bin/myapp', 'myapp', options(which='/usr/bin')
70+
posix_not_in_path = '/opt/myapp/bin/myapp', '/opt/myapp/bin/myapp'
71+
posix_not_from_path = '/opt/myapp/bin/myapp', '/opt/myapp/bin/myapp', options(which='/usr/bin')
72+
posix_relpath = 'myapp/bin/myapp', 'myapp/bin/myapp', options(which='')
73+
posix_parentpath_also_in_path = '../myapp/bin/myapp', '../myapp/bin/myapp', options(which='')
74+
posix_parentpath = '../myapp/bin/myapp', '../myapp/bin/myapp', options(which=None)
75+
76+
with options(to_path=pathlib.PureWindowsPath):
77+
win_dotpy = "C:/a/path/leading/to/myapp.py", r"C:\a\path\leading\to\myapp.py"
78+
win_in_path = 'C:/Program Files/myapp/bin/myapp.py', 'myapp.py', options(which='C:/Program Files/myapp/bin/')
79+
win_not_in_path = 'C:/Users/Myself/Documents/myapp', r'C:\Users\Myself\Documents\myapp'
80+
win_not_from_path = 'C:/Users/Myself/Documents/myapp', r'C:\Users\Myself\Documents\myapp', options(which='C:/system32')
81+
win_relpath = './my/folder/myapp.py', r'my\folder\myapp.py', options(which=None)
82+
win_parentpath_also_in_path = '../myapp/bin/myapp', r'..\myapp\bin\myapp', options(which='')
83+
win_parentpath = '../myapp/bin/myapp', r'..\myapp\bin\myapp', options(which=None)
84+
win_diff_drives = 'D:/myapp/bin/myapp', r"D:\myapp\bin\myapp", options(which="C:/myapp/bin/myapp")
9285

9386

94-
def get_executable(path, default):
95-
return default
87+
def get_executable_verbatim(path):
88+
return path
9689

9790

91+
@repeated_test.with_options_matrix(platform=["anythingreally", "win32"], executable=["interpreter"], get_executable=[get_executable_verbatim])
9892
class FixArgvTests(Fixtures):
99-
def _test(self, argv, path, main, expect):
100-
def get_executable(path, default):
101-
return default
102-
_get_executable = runner.get_executable
103-
runner.get_executable = get_executable
104-
try:
105-
module = MockModule(*main)
106-
self.assertEqual(expect, runner.fix_argv(argv, path, module))
107-
finally:
108-
runner.get_executable = _get_executable
93+
def _test(self, argv, path, main, expect, *, platform, executable, get_executable):
94+
module = MockModule(*main)
95+
self.assertEqual(expect, runner._fix_argv(argv, path, module, executable=executable, platform=platform, get_executable=get_executable))
10996

11097
plainfile = (
11198
['afile.py', '...'], ['/path/to/cwd', '/usr/lib/pythonX.Y'],
11299
['afile.py', '__main__', None],
113-
['afile.py', '...']
114-
)
100+
['afile.py', '...'],
101+
options(platform="anythingreally"),
102+
)
103+
104+
plainfile_win = (
105+
['afile.py', '...'], ['/path/to/cwd', '/usr/lib/pythonX.Y'],
106+
['afile.py', '__main__', None],
107+
['interpreter afile.py', '...'],
108+
options(platform="win32"),
109+
)
110+
115111
asmodule = (
116112
['/path/to/cwd/afile.py', '...'], ['', '/usr/lib/pythonX.Y'],
117113
['/path/to/cwd/afile.py', '__main__', ''],
118-
['python -m afile', '...']
114+
['interpreter -m afile', '...']
119115
)
120116
packedmodule = (
121117
['/path/to/cwd/apkg/afile.py', '...'], ['', '/usr/lib/pythonX.Y'],
122118
['/path/to/cwd/apkg/afile.py', '__main__', 'apkg'],
123-
['python -m apkg.afile', '...']
119+
['interpreter -m apkg.afile', '...']
124120
)
125121
packedmain2 = (
126122
['/path/to/cwd/apkg/__main__.py', '...'], ['', '/usr/lib/pythonX.Y'],
127123
['/path/to/cwd/apkg/__main__.py', 'apkg.__main__', 'apkg'],
128-
['python -m apkg', '...']
124+
['interpreter -m apkg', '...']
129125
)
130126
packedmain3 = (
131127
['/path/to/cwd/apkg/__main__.py', '...'], ['', '/usr/lib/pythonX.Y'],
132128
['/path/to/cwd/apkg/__main__.py', '__main__', 'apkg'],
133-
['python -m apkg', '...']
129+
['interpreter -m apkg', '...']
134130
)
135131

136132
def test_bad_fakemodule(self):
137-
back = runner.get_executable
138-
runner.get_executable = get_executable
139-
try:
140-
module = BadModule('/path/to/cwd/afile.py', '__main__')
141-
argv = ['afile.py', '...']
142-
path = ['', '/usr/lib/pythonX.Y']
143-
self.assertEqual(['afile.py', '...'],
144-
runner.fix_argv(argv, path, module))
145-
finally:
146-
runner.get_executable = back
133+
module = BadModule('/path/to/cwd/afile.py', '__main__')
134+
argv = ['afile.py', '...']
135+
path = ['', '/usr/lib/pythonX.Y']
136+
self.assertEqual(['afile.py', '...'],
137+
runner._fix_argv(argv, path, module, get_executable=lambda p: ''))
147138

148139

149140
class GetCliTests(unittest.TestCase):
@@ -511,27 +502,24 @@ def test_run_sysargv(self):
511502
bmodules = sys.modules
512503
bargv = sys.argv
513504
bpath = sys.path
514-
bget_executable = runner.get_executable
515505
try:
516506
sys.modules['__main__'] \
517507
= MockModule('/path/to/cwd/afile.py', '__main__', '')
518508
sys.argv = ['afile.py', '...']
519509
sys.path = [''] + sys.path[1:]
520-
runner.get_executable = get_executable
521510
def func(arg=1):
522511
raise NotImplementedError
523512
out = StringIO()
524513
err = StringIO()
525514
runner.run(func, exit=False, out=out, err=err)
526515
self.assertFalse(out.getvalue())
527-
self.assertEqual(err.getvalue(),
528-
"python -m afile: Bad value for arg: '...'\n"
529-
"Usage: python -m afile [arg]\n")
516+
self.assertRegex(err.getvalue(),
517+
".* -m afile: Bad value for arg: '...'\n"
518+
r"Usage: .* -m afile \[arg\]" "\n")
530519
finally:
531520
sys.modules = bmodules
532521
sys.argv = bargv
533522
sys.path = bpath
534-
runner.get_executable = bget_executable
535523

536524
def test_run_out(self):
537525
bout = sys.stdout

clize/tests/test_testutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from . import util
1+
from clize.tests import util
22

33

44
class AssertLinesEqualTests(util.Fixtures):

0 commit comments

Comments
 (0)