Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/manage/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,8 @@ def __init__(self, args, root=None):
set_next = a.lstrip("-/").lower()
try:
key, value, *opts = cmd_args[set_next]
except LookupError:
raise ArgumentError(f"Unexpected argument: {a}")
except KeyError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to raise IndexError (or LookupError directly) here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An unpacking error will raise IndexError, which should never happen, so it should bubble out as an internal error rather than unexpected argument.

raise ArgumentError(f"Unexpected argument: {a}") from None
if value is _NEXT:
if sep:
if opts:
Expand Down Expand Up @@ -868,6 +868,10 @@ class HelpCommand(BaseCommand):

_create_log_file = False

def __init__(self, args, root=None):
super().__init__([self.CMD], root)
self.args = [a for a in args[1:] if a.isalpha()]

def execute(self):
LOGGER.print(COPYRIGHT)
self.show_welcome(copyright=False)
Expand Down
20 changes: 11 additions & 9 deletions src/manage/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,24 @@
return v.lower().startswith(("t", "y", "1"))
return bool(v)

def _global_file():

def load_global_config(cfg, schema):
try:
from _native import package_get_root
except ImportError:
return Path(sys.executable).parent / DEFAULT_CONFIG_NAME
return Path(package_get_root()) / DEFAULT_CONFIG_NAME
file = Path(sys.executable).parent / DEFAULT_CONFIG_NAME

Check warning on line 43 in src/manage/config.py

View check run for this annotation

Codecov / codecov/patch

src/manage/config.py#L43

Added line #L43 was not covered by tests
else:
file = Path(package_get_root()) / DEFAULT_CONFIG_NAME
try:
load_one_config(cfg, file, schema=schema)
except FileNotFoundError:
pass

Check warning on line 49 in src/manage/config.py

View check run for this annotation

Codecov / codecov/patch

src/manage/config.py#L45-L49

Added lines #L45 - L49 were not covered by tests


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

global_file = _global_file()
if global_file:
try:
load_one_config(cfg, global_file, schema=schema)
except FileNotFoundError:
pass
load_global_config(cfg, schema=schema)

try:
reg_cfg = load_registry_config(cfg["registry_override_key"], schema=schema)
Expand Down
14 changes: 7 additions & 7 deletions src/manage/list_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,39 +167,39 @@


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

Check warning on line 170 in src/manage/list_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/list_command.py#L170

Added line #L170 was not covered by tests


def format_json_lines(cmd, installs):
for i in installs:
print(json.dumps(i, default=str))
LOGGER.print_raw(json.dumps(i, default=str))

Check warning on line 175 in src/manage/list_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/list_command.py#L175

Added line #L175 was not covered by tests


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

Check warning on line 183 in src/manage/list_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/list_command.py#L183

Added line #L183 was not covered by tests


def format_bare_exe(cmd, installs):
for i in installs:
print(i["executable"])
LOGGER.print_raw(i["executable"])

Check warning on line 188 in src/manage/list_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/list_command.py#L188

Added line #L188 was not covered by tests


def format_bare_prefix(cmd, installs):
for i in installs:
try:
print(i["prefix"])
LOGGER.print_raw(i["prefix"])

Check warning on line 194 in src/manage/list_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/list_command.py#L194

Added line #L194 was not covered by tests
except KeyError:
pass


def format_bare_url(cmd, installs):
for i in installs:
try:
print(i["url"])
LOGGER.print_raw(i["url"])

Check warning on line 202 in src/manage/list_command.py

View check run for this annotation

Codecov / codecov/patch

src/manage/list_command.py#L202

Added line #L202 was not covered by tests
except KeyError:
pass

Expand All @@ -223,7 +223,7 @@
if not seen_default and i.get("default"):
tag = f"{tag} *"
seen_default = True
print(tag.ljust(17), i["executable"] if paths else i["display-name"])
LOGGER.print_raw(tag.ljust(17), i["executable"] if paths else i["display-name"])


FORMATTERS = {
Expand Down
45 changes: 35 additions & 10 deletions src/manage/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,19 @@


class Logger:
def __init__(self):
if os.getenv("PYMANAGER_DEBUG"):
def __init__(self, level=None, console=sys.stderr, print_console=sys.stdout):
if level is not None:
self.level = level

Check warning on line 77 in src/manage/logging.py

View check run for this annotation

Codecov / codecov/patch

src/manage/logging.py#L77

Added line #L77 was not covered by tests
elif os.getenv("PYMANAGER_DEBUG"):
self.level = DEBUG
elif os.getenv("PYMANAGER_VERBOSE"):
self.level = VERBOSE
else:
self.level = INFO
self.console = sys.stderr
self.console = console
self.console_colour = supports_colour(self.console)
self.print_console = print_console
self.print_console_colour = supports_colour(self.print_console)
self.file = None
self._list = None

Expand Down Expand Up @@ -158,13 +162,19 @@
return False
return True

def print(self, msg=None, *args, always=False, level=INFO, **kwargs):
def print(self, msg=None, *args, always=False, level=INFO, colours=True, **kwargs):
if self._list is not None:
self._list.append(((msg or "") % args, ()))
if args:
self._list.append(((msg or "") % args, ()))
else:
self._list.append((msg or "", ()))
if not always and level < self.level:
return
if msg:
if self.console_colour:
if not colours:
# Don't unescape or replace anything
pass
elif self.print_console_colour:
for k in COLOURS:
msg = msg.replace(k, COLOURS[k])
else:
Expand All @@ -176,7 +186,13 @@
msg = str(args[0])
else:
msg = ""
print(msg, **kwargs, file=self.console)
print(msg, **kwargs, file=self.print_console)

def print_raw(self, *msg, **kwargs):
kwargs["always"] = True
kwargs["colours"] = False
sep = kwargs.pop("sep", " ")
return self.print(sep.join(str(s) for s in msg), **kwargs)


LOGGER = Logger()
Expand All @@ -201,7 +217,10 @@
if self._complete:
LOGGER.print()
else:
LOGGER.print("❌")
try:
LOGGER.print("❌")
except UnicodeEncodeError:
LOGGER.print("x")

Check warning on line 223 in src/manage/logging.py

View check run for this annotation

Codecov / codecov/patch

src/manage/logging.py#L220-L223

Added lines #L220 - L223 were not covered by tests

def __call__(self, progress):
if self._complete:
Expand All @@ -210,7 +229,10 @@
if progress is None:
if self._need_newline:
if not self._complete:
LOGGER.print("⏸️")
try:
LOGGER.print("⏸️")
except UnicodeEncodeError:
LOGGER.print("|")

Check warning on line 235 in src/manage/logging.py

View check run for this annotation

Codecov / codecov/patch

src/manage/logging.py#L232-L235

Added lines #L232 - L235 were not covered by tests
self._dots_shown = 0
self._started = False
self._need_newline = False
Expand All @@ -229,6 +251,9 @@
LOGGER.print(None, "." * dot_count, end="", flush=True)
self._need_newline = True
if progress >= 100:
LOGGER.print("✅", flush=True)
try:
LOGGER.print("✅", flush=True)
except UnicodeEncodeError:
LOGGER.print(".", flush=True)

Check warning on line 257 in src/manage/logging.py

View check run for this annotation

Codecov / codecov/patch

src/manage/logging.py#L254-L257

Added lines #L254 - L257 were not covered by tests
self._complete = True
self._need_newline = False
128 changes: 117 additions & 11 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
import winreg

from pathlib import Path
from pathlib import Path, PurePath

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

Expand All @@ -18,41 +18,83 @@
setattr(_native, k, getattr(_native_test, k))


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


import manage.commands
manage.commands.WELCOME = ""


from manage.logging import LOGGER, DEBUG
from manage.logging import LOGGER, DEBUG, ERROR
LOGGER.level = DEBUG

import manage.config
import manage.installs


# Ensure we don't pick up any settings from configs or the registry

def _mock_load_global_config(cfg, schema):
cfg.update({
"base_config": "",
"user_config": "",
"additional_config": "",
})

def _mock_load_registry_config(key, schema):
return {}

Check warning on line 46 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L46

Added line #L46 was not covered by tests

manage.config.load_global_config = _mock_load_global_config
manage.config.load_registry_config = _mock_load_registry_config


@pytest.fixture
def quiet_log():
lvl = LOGGER.level
LOGGER.level = ERROR
try:
yield

Check warning on line 57 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L54-L57

Added lines #L54 - L57 were not covered by tests
finally:
LOGGER.level = lvl

Check warning on line 59 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L59

Added line #L59 was not covered by tests


class LogCaptureHandler(list):
def skip_until(self, pattern, args=()):
return ('until', pattern, args)

def not_logged(self, pattern, args=()):
return ('not', pattern, args)

def __call__(self, *cmp):
it1 = iter(self)
i = 0
for y in cmp:
if not isinstance(y, tuple):
op, pat, args = None, y, []
op, pat, args = None, y, None
elif len(y) == 3:
op, pat, args = y
elif len(y) == 2:
op = None
pat, args = y

if op == 'not':
for j in range(i, len(self)):
if re.match(pat, self[j][0], flags=re.S):
pytest.fail(f"Should not have found {self[j][0]!r} matching {pat}")
return

Check warning on line 84 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L83-L84

Added lines #L83 - L84 were not covered by tests
continue

while True:
try:
x = next(it1)
except StopIteration:
x = self[i]
i += 1
except IndexError:

Check warning on line 91 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L91

Added line #L91 was not covered by tests
pytest.fail(f"Not enough elements were logged looking for {pat}")
if op == 'until' and not re.match(pat, x[0]):
if op == 'until' and not re.match(pat, x[0], flags=re.S):
continue
assert re.match(pat, x[0])
assert tuple(x[1]) == tuple(args)
assert re.match(pat, x[0], flags=re.S)
if args is not None:
assert tuple(x[1]) == tuple(args)
break


Expand Down Expand Up @@ -150,3 +192,67 @@
def registry():
with RegistryFixture(winreg.HKEY_CURRENT_USER, REG_TEST_ROOT) as key:
yield key



def make_install(tag, **kwargs):
run_for = []
for t in kwargs.get("run_for", [tag]):
run_for.append({"tag": t, "target": kwargs.get("target", "python.exe")})
run_for.append({"tag": t, "target": kwargs.get("targetw", "pythonw.exe"), "windowed": 1})

return {
"company": kwargs.get("company", "PythonCore"),
"id": "{}-{}".format(kwargs.get("company", "PythonCore"), tag),
"sort-version": kwargs.get("sort_version", tag),
"display-name": "{} {}".format(kwargs.get("company", "Python"), tag),
"tag": tag,
"install-for": [tag],
"run-for": run_for,
"prefix": PurePath(kwargs.get("prefix", rf"C:\{tag}")),
"executable": kwargs.get("executable", "python.exe"),
}


def fake_get_installs(install_dir):
yield make_install("1.0")
yield make_install("1.0-32", sort_version="1.0")
yield make_install("1.0-64", sort_version="1.0")
yield make_install("2.0-64", sort_version="2.0")
yield make_install("2.0-arm64", sort_version="2.0")
yield make_install("3.0a1-32", sort_version="3.0a1")
yield make_install("3.0a1-64", sort_version="3.0a1")
yield make_install("1.1", company="Company", target="company.exe", targetw="companyw.exe")
yield make_install("1.1-64", sort_version="1.1", company="Company", target="company.exe", targetw="companyw.exe")
yield make_install("1.1-arm64", sort_version="1.1", company="Company", target="company.exe", targetw="companyw.exe")
yield make_install("2.1", sort_version="2.1", company="Company", target="company.exe", targetw="companyw.exe")
yield make_install("2.1-64", sort_version="2.1", company="Company", target="company.exe", targetw="companyw.exe")


def fake_get_installs2(install_dir):
yield make_install("1.0-32", sort_version="1.0")
yield make_install("3.0a1-32", sort_version="3.0a1", run_for=["3.0.1a1-32", "3.0-32", "3-32"])
yield make_install("3.0a1-64", sort_version="3.0a1", run_for=["3.0.1a1-64", "3.0-64", "3-64"])
yield make_install("3.0a1-arm64", sort_version="3.0a1", run_for=["3.0.1a1-arm64", "3.0-arm64", "3-arm64"])


def fake_get_unmanaged_installs():
return []


def fake_get_venv_install(virtualenv):
raise LookupError

Check warning on line 244 in tests/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/conftest.py#L244

Added line #L244 was not covered by tests


@pytest.fixture
def patched_installs(monkeypatch):
monkeypatch.setattr(manage.installs, "_get_installs", fake_get_installs)
monkeypatch.setattr(manage.installs, "_get_unmanaged_installs", fake_get_unmanaged_installs)
monkeypatch.setattr(manage.installs, "_get_venv_install", fake_get_venv_install)


@pytest.fixture
def patched_installs2(monkeypatch):
monkeypatch.setattr(manage.installs, "_get_installs", fake_get_installs2)
monkeypatch.setattr(manage.installs, "_get_unmanaged_installs", fake_get_unmanaged_installs)
monkeypatch.setattr(manage.installs, "_get_venv_install", fake_get_venv_install)
Loading