8000 Improves help output and testability of commands. (#80) · python/pymanager@9c81127 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9c81127

Browse files
authored
Improves help output and testability of commands. (#80)
Fixes #74
1 parent d2d8e2e commit 9c81127

File tree

7 files changed

+204
-103
lines changed

7 files changed

+204
-103
lines changed

src/manage/commands.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,8 @@ def __init__(self, args, root=None):
353353
set_next = a.lstrip("-/").lower()
354354
try:
355355
key, value, *opts = cmd_args[set_next]
356-
except LookupError:
357-
raise ArgumentError(f"Unexpected argument: {a}")
356+
except KeyError:
357+
raise ArgumentError(f"Unexpected argument: {a}") from None
358358
if value is _NEXT:
359359
if sep:
360360
if opts:
@@ -868,6 +868,10 @@ class HelpCommand(BaseCommand):
868868

869869
_create_log_file = False
870870

871+
def __init__(self, args, root=None):
872+
super().__init__([self.CMD], root)
873+
self.args = [a for a in args[1:] if a.isalpha()]
874+
871875
def execute(self):
872876
LOGGER.print(COPYRIGHT)
873877
self.show_welcome(copyright=False)

src/manage/config.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,24 @@ def config_bool(v):
3535
return v.lower().startswith(("t", "y", "1"))
3636
return bool(v)
3737

38-
def _global_file():
38+
39+
def load_global_config(cfg, schema):
3940
try:
4041
from _native import package_get_root
4142
except ImportError:
42-
return Path(sys.executable).parent / DEFAULT_CONFIG_NAME
43-
return Path(package_get_root()) / DEFAULT_CONFIG_NAME
43+
file = Path(sys.executable).parent / DEFAULT_CONFIG_NAME
44+
else:
45+
file = Path(package_get_root()) / DEFAULT_CONFIG_NAME
46+
try:
47+
load_one_config(cfg, file, schema=schema)
48+
except FileNotFoundError:
49+
pass
50+
4451

4552
def load_config(root, override_file, schema):
4653
cfg = {}
4754

48-
global_file = _global_file()
49-
if global_file:
50-
try:
51-
load_one_config(cfg, global_file, schema=schema)
52-
except FileNotFoundError:
53-
pass
55+
load_global_config(cfg, schema=schema)
5456

5557
try:
5658
reg_cfg = load_registry_config(cfg["registry_override_key"], schema=schema)

src/manage/list_command.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,39 +167,39 @@ def format_csv(cmd, installs):
167167

168168

169169
def format_json(cmd, installs):
170-
print(json.dumps({"versions": installs}, default=str))
170+
LOGGER.print_raw(json.dumps({"versions": installs}, default=str))
171171

172172

173173
def format_json_lines(cmd, installs):
174174
for i in installs:
175-
print(json.dumps(i, default=str))
175+
LOGGER.print_raw(json.dumps(i, default=str))
176176

177177

178178
def format_bare_id(cmd, installs):
179179
for i in installs:
180180
# Don't print useless values (__active-virtual-env, __unmanaged-)
181181
if i["id"].startswith("__"):
182182
continue
183-
print(i["id"])
183+
LOGGER.print_raw(i["id"])
184184

185185

186186
def format_bare_exe(cmd, installs):
187187
for i in installs:
188-
print(i["executable"])
188+
LOGGER.print_raw(i["executable"])
189189

190190

191191
def format_bare_prefix(cmd, installs):
192192
for i in installs:
193193
try:
194-
print(i["prefix"])
194+
LOGGER.print_raw(i["prefix"])
195195
except KeyError:
196196
pass
197197

198198

199199
def format_bare_url(cmd, installs):
200200
for i in installs:
201201
try:
202-
print(i["url"])
202+
LOGGER.print_raw(i["url"])
203203
except KeyError:
204204
pass
205205

@@ -223,7 +223,7 @@ def format_legacy(cmd, installs, paths=False):
223223
if not seen_default and i.get("default"):
224224
tag = f"{tag} *"
225225
seen_default = True
226-
print(tag.ljust(17), i["executable"] if paths else i["display-name"])
226+
LOGGER.print_raw(tag.ljust(17), i["executable"] if paths else i["display-name"])
227227

228228

229229
FORMATTERS = {

src/manage/logging.py

Lines changed: 35 additions & 10 deletions
F987
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,19 @@ def supports_colour(stream):
7272

7373

7474
class Logger:
75-
def __init__(self):
76-
if os.getenv("PYMANAGER_DEBUG"):
75+
def __init__(self, level=None, console=sys.stderr, print_console=sys.stdout):
76+
if level is not None:
77+
self.level = level
78+
elif os.getenv("PYMANAGER_DEBUG"):
7779
self.level = DEBUG
7880
elif os.getenv("PYMANAGER_VERBOSE"):
7981
self.level = VERBOSE
8082
else:
8183
self.level = INFO
82-
self.console = sys.stderr
84+
self.console = console
8385
self.console_colour = supports_colour(self.console)
86+
self.print_console = print_console
87+
self.print_console_colour = supports_colour(self.print_console)
8488
self.file = None
8589
self._list = None
8690

@@ -158,13 +162,19 @@ def would_print(self, *args, always=False, level=INFO, **kwargs):
158162
return False
159163
return True
160164

161-
def print(self, msg=None, *args, always=False, level=INFO, **kwargs):
165+
def print(self, msg=None, *args, always=False, level=INFO, colours=True, **kwargs):
162166
if self._list is not None:
163-
self._list.append(((msg or "") % args, ()))
167+
if args:
168+
self._list.append(((msg or "") % args, ()))
169+
else:
170+
self._list.append((msg or "", ()))
164171
if not always and level < self.level:
165172
return
166173
if msg:
167-
if self.console_colour:
174+
if not colours:
175+
# Don't unescape or replace anything
176+
pass
177+
elif self.print_console_colour:
168178
for k in COLOURS:
169179
msg = msg.replace(k, COLOURS[k])
170180
else:
@@ -176,7 +186,13 @@ def print(self, msg=None, *args, always=False, level=INFO, **kwargs):
176186
msg = str(args[0])
177187
else:
178188
msg = ""
179-
print(msg, **kwargs, file=self.console)
189+
print(msg, **kwargs, file=self.print_console)
190+
191+
def print_raw(self, *msg, **kwargs):
192+
kwargs["always"] = True
193+
kwargs["colours"] = False
194+
sep = kwargs.pop("sep", " ")
195+
return self.print(sep.join(str(s) for s in msg), **kwargs)
180196

181197

182198
LOGGER = Logger()
@@ -201,7 +217,10 @@ def __exit__(self, *exc_info):
201217
if self._complete:
202218
LOGGER.print()
203219
else:
204-
LOGGER.print("❌")
220+
try:
221+
LOGGER.print("❌")
222+
except UnicodeEncodeError:
223+
LOGGER.print("x")
205224

206225
def __call__(self, progress):
207226
if self._complete:
@@ -210,7 +229,10 @@ def __call__(self, progress):
210229
if progress is None:
211230
if self._need_newline:
212231
if not self._complete:
213-
LOGGER.print("⏸️")
232+
try:
233+
LOGGER.print("⏸️")
234+
except UnicodeEncodeError:
235+
LOGGER.print("|")
214236
self._dots_shown = 0
215237
self._started = False
216238
self._need_newline = False
@@ -229,6 +251,9 @@ def __call__(self, progress):
229251
LOGGER.print(None, "." * dot_count, end="", flush=True)
230252
self._need_newline = True
231253
if progress >= 100:
232-
LOGGER.print("✅", flush=True)
254+
try:
255+
LOGGER.print("✅", flush=True)
256+
except UnicodeEncodeError:
257+
LOGGER.print(".", flush=True)
233258
self._complete = True
234259
self._need_newline = False

tests/conftest.py

Lines changed: 117 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import sys
77
import winreg
88

9-
from pathlib import Path
9+
from pathlib import Path, PurePath
1010

1111
TESTS = Path(__file__).absolute().parent
1212

@@ -18,41 +18,83 @@
1818
setattr(_native, k, getattr(_native_test, k))
1919

2020

21+
# Importing in order carefully to ensure the variables we override are handled
22+
# correctly by submodules.
2123
import manage
2224
manage.EXE_NAME = "pymanager-pytest"
2325

24-
2526
import manage.commands
2627
manage.commands.WELCOME = ""
2728

28-
29-
from manage.logging import LOGGER, DEBUG
29+
from manage.logging import LOGGER, DEBUG, ERROR
3030
LOGGER.level = DEBUG
3131

32+
import manage.config
33+
import manage.installs
34+
35+
36+
# Ensure we don't pick up any settings from configs or the registry
37+
38+
def _mock_load_global_config(cfg, schema):
39+
cfg.update({
40+
"base_config": "",
41+
"user_config": "",
42+
"additional_config": "",
43+
})
44+
45+
def _mock_load_registry_config(key, schema):
46+
return {}
47+
48+
manage.config.load_global_config = _mock_load_global_config
49+
manage.config.load_registry_config = _mock_load_registry_config
50+
51+
52+
@pytest.fixture
53+
def quiet_log():
54+
lvl = LOGGER.level
55+
LOGGER.level = ERROR
56+
try:
57+
yield
58+
finally:
59+
LOGGER.level = lvl
60+
61+
3262
class LogCaptureHandler(list):
3363
def skip_until(self, pattern, args=()):
3464
return ('until', pattern, args)
3565

66+
def not_logged(self, pattern, args=()):
67+
return ('not', pattern, args)
68+
3669
def __call__(self, *cmp):
37-
it1 = iter(self)
70+
i = 0
3871
for y in cmp:
3972
if not isinstance(y, tuple):
40-
op, pat, args = None, y, []
73+
op, pat, args = None, y, None
4174
elif len(y) == 3:
4275
op, pat, args = y
4376
elif len(y) == 2:
4477
op = None
4578
pat, args = y
4679

80+
if op == 'not':
81+
for j in range(i, len(self)):
82+
if re.match(pat, self[j][0], flags=re.S):
83+
pytest.fail(f"Should not have found {self[j][0]!r} matching {pat}")
84+
return
85+
continue
86+
4787
while True:
4888
try:
49-
x = next(it1)
50-
except StopIteration:
89+
x = self[i]
90+
i += 1
91+
except IndexError:
5192
pytest.fail(f"Not enough elements were logged looking for {pat}")
52-
if op == 'until' and not re.match(pat, x[0]):
93+
if op == 'until' and not re.match(pat, x[0], flags=re.S):
5394
continue
54-
assert re.match(pat, x[0])
55-
assert tuple(x[1]) == tuple(args)
95+
assert re.match(pat, x[0], flags=re.S)
96+
if args is not None:
97+
assert tuple(x[1]) == tuple(args)
5698
break
5799

58100

@@ -150,3 +192,67 @@ def setup(self, _subkey=None, **keys):
150192
def registry():
151193
with RegistryFixture(winreg.HKEY_CURRENT_USER, REG_TEST_ROOT) as key:
152194
yield key
195+
196+
197+
198+
def make_install(tag, **kwargs):
199+
run_for = []
200+
for t in kwargs.get("run_for", [tag]):
201+
run_for.append({"tag": t, "target": kwargs.get("target", "python.exe")})
202+
run_for.append({"tag": t, "target": kwargs.get("targetw", "pythonw.exe"), "windowed": 1})
203+
204+
return {
205+
"company": kwargs.get("company", "PythonCore"),
206+
"id": "{}-{}".format(kwargs.get("company", "PythonCore"), tag),
207+
"sort-version": kwargs.get("sort_version", tag),
208+
"display-name": "{} {}".format(kwargs.get("company", "Python"), tag),
209+
"tag": tag,
210+
"install-for": [tag],
211+
"run-for": run_for,
212+
"prefix": PurePath(kwargs.get("prefix", rf"C:\{tag}")),
213+
"executable": kwargs.get("executable", "python.exe"),
214+
}
215+
216+
217+
def fake_get_installs(install_dir):
218+
yield make_install("1.0")
219+
yield make_install("1.0-32", sort_version="1.0")
220+
yield make_install("1.0-64", sort_version="1.0")
221+
yield make_install("2.0-64", sort_version="2.0")
222+
yield make_install("2.0-arm64", sort_version="2.0")
223+
yield make_install("3.0a1-32", sort_version="3.0a1")
224+
yield make_install("3.0a1-64", sort_version="3.0a1")
225+
yield make_install("1.1", company="Company", target="company.exe", targetw="companyw.exe")
226+
yield make_install("1.1-64", sort_version="1.1", company="Company", target="company.exe", targetw="companyw.exe")
227+
yield make_install("1.1-arm64", sort_version="1.1", company="Company", target="company.exe", targetw="companyw.exe")
228+
yield make_install("2.1", sort_version="2.1", company="Company", target="company.exe", targetw="companyw.exe")
229+
yield make_install("2.1-64", sort_version="2.1", company="Company", target="company.exe", targetw="companyw.exe")
230+
231+
232+
def fake_get_installs2(install_dir):
233+
yield make_install("1.0-32", sort_version="1.0")
234+
yield make_install("3.0a1-32", sort_version="3.0a1", run_for=["3.0.1a1-32", "3.0-32", "3-32"])
235+
yield make_install("3.0a1-64", sort_version="3.0a1", run_for=["3.0.1a1-64", "3.0-64", "3-64"])
236+
yield make_install("3.0a1-arm64", sort_version="3.0a1", run_for=["3.0.1a1-arm64", "3.0-arm64", "3-arm64"])
237+
238+
239+
def fake_get_unmanaged_installs():
240+
return []
241+
242+
243+
def fake_get_venv_install(virtualenv):
244+
raise LookupError
245+
246+
247+
@pytest.fixture
248+
def patched_installs(monkeypatch):
249+
monkeypatch.setattr(manage.installs, "_get_installs", fake_get_installs)
250+
monkeypatch.setattr(manage.installs, "_get_unmanaged_installs", fake_get_unmanaged_installs)
251+
monkeypatch.setattr(manage.installs, "_get_venv_install", fake_get_venv_install)
252+
253+
254+
@pytest.fixture
255+
def patched_installs2(monkeypatch):
256+
monkeypatch.setattr(manage.installs, "_get_installs", fake_get_installs2)
257+
monkeypatch.setattr(manage.installs, "_get_unmanaged_installs", fake_get_unmanaged_installs)
258+
monkeypatch.setattr(manage.installs, "_get_venv_install", fake_get_venv_install)

0 commit comments

Comments
 (0)
0