diff --git a/.bumpversion.toml b/.bumpversion.toml index 9f97c6700..ceed6f882 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,6 +1,6 @@ [tool.bumpversion] allow_dirty = true - current_version = "0.184.2" + current_version = "0.184.3" [[tool.bumpversion.files]] filename = "pyproject.toml" diff --git a/pyproject.toml b/pyproject.toml index 4250c9abd..43620c5ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ "coloredlogs>=15.0.1", "coverage-conditional-plugin>=0.9.0", "dycw-pytest-only>=2.1.1", - "dycw-utilities[test]>=0.184.1", + "dycw-utilities[test]>=0.184.2", "pyright>=1.1.408", "pytest-cov>=7.0.0", "pytest-timeout>=2.4.0", @@ -116,7 +116,7 @@ name = "dycw-utilities" readme = "README.md" requires-python = ">= 3.12" - version = "0.184.2" + version = "0.184.3" [project.entry-points.pytest11] pytest-randomly = "utilities.pytest_plugins.pytest_randomly" diff --git a/src/tests/test_subprocess.py b/src/tests/test_subprocess.py index fbf673a03..2e1aa0288 100644 --- a/src/tests/test_subprocess.py +++ b/src/tests/test_subprocess.py @@ -35,8 +35,10 @@ _rsync_many_prepare, _ssh_is_strict_checking_error, _uv_pip_list_assemble_output, - _UvPipListBaseError, - _UvPipListOutdatedError, + _uv_pip_list_loads, + _UvPipListBaseVersionError, + _UvPipListJsonError, + _UvPipListOutdatedVersionError, _UvPipListOutput, append_text, apt_install_cmd, @@ -1776,6 +1778,24 @@ def test_main(self) -> None: assert is_sequence_of(result, _UvPipListOutput) +class TestUvPipListLoadsOutput: + def test_main(self) -> None: + text = strip_and_dedent(""" + [{"name":"name","version":"0.0.1"}] + """) + result = _uv_pip_list_loads(text) + expected = [{"name": "name", "version": "0.0.1"}] + assert result == expected + + def test_error(self) -> None: + text = strip_and_dedent(""" + [{"name":"name","version":"0.0.1"}] + # warning: The package `name` requires `dep>=1.2.3`, but `1.2.2` is installed + """) + with raises(_UvPipListJsonError, match=r"Unable to parse JSON; got '.*'"): + _ = _uv_pip_list_loads(text) + + class TestUvPipListAssembleOutput: def test_main(self) -> None: dict_ = {"name": "name", "version": "0.0.1"} @@ -1819,13 +1839,17 @@ def test_outdated(self) -> None: def test_error_base(self) -> None: dict_ = {"name": "name", "version": "invalid"} outdated = [] - with raises(_UvPipListBaseError, match=r"Unable to parse version; got .*"): + with raises( + _UvPipListBaseVersionError, match=r"Unable to parse version; got .*" + ): _ = _uv_pip_list_assemble_output(dict_, outdated) def test_error_outdated(self) -> None: dict_ = {"name": "name", "version": "0.0.1"} outdated = [{"name": "name", "latest_version": "invalid"}] - with raises(_UvPipListOutdatedError, match=r"Unable to parse version; got .*"): + with raises( + _UvPipListOutdatedVersionError, match=r"Unable to parse version; got .*" + ): _ = _uv_pip_list_assemble_output(dict_, outdated) diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py index 139749ed3..f4ed85fc4 100644 --- a/src/utilities/__init__.py +++ b/src/utilities/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "0.184.2" +__version__ = "0.184.3" diff --git a/src/utilities/subprocess.py b/src/utilities/subprocess.py index 8b51844e9..508e3d582 100644 --- a/src/utilities/subprocess.py +++ b/src/utilities/subprocess.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from io import StringIO from itertools import chain, repeat +from json import JSONDecodeError from pathlib import Path from re import MULTILINE, escape, search from shlex import join @@ -1784,14 +1785,21 @@ def uv_pip_list( for outdated in [False, True] ] text_base, text_outdated = [ - run(*cmds, return_=True) for cmds in [cmds_base, cmds_outdated] - ] - dicts_base, dicts_outdated = [ - json.loads(text) for text in [text_base, text_outdated] + run(*cmds, return_stdout=True) for cmds in [cmds_base, cmds_outdated] ] + dicts_base, dicts_outdated = list( + map(_uv_pip_list_loads, [text_base, text_outdated]) + ) return [_uv_pip_list_assemble_output(d, dicts_outdated) for d in dicts_base] +def _uv_pip_list_loads(text: str, /) -> list[StrMapping]: + try: + return json.loads(text) + except JSONDecodeError: + raise _UvPipListJsonError(text=text) from None + + def _uv_pip_list_assemble_output( dict_: StrMapping, outdated: Iterable[StrMapping], / ) -> _UvPipListOutput: @@ -1799,7 +1807,7 @@ def _uv_pip_list_assemble_output( try: version = parse_version_2_or_3(dict_["version"]) except ParseVersion2Or3Error: - raise _UvPipListBaseError(data=dict_) from None + raise _UvPipListBaseVersionError(data=dict_) from None try: location = Path(dict_["editable_project_location"]) except KeyError: @@ -1812,7 +1820,7 @@ def _uv_pip_list_assemble_output( try: latest_version = parse_version_2_or_3(outdated_i["latest_version"]) except ParseVersion2Or3Error: - raise _UvPipListOutdatedError(data=outdated_i) from None + raise _UvPipListOutdatedVersionError(data=outdated_i) from None latest_filetype = outdated_i["latest_filetype"] return _UvPipListOutput( name=dict_["name"], @@ -1824,7 +1832,20 @@ def _uv_pip_list_assemble_output( @dataclass(kw_only=True, slots=True) -class UvPipListError(Exception): +class UvPipListError(Exception): ... + + +@dataclass(kw_only=True, slots=True) +class _UvPipListJsonError(UvPipListError): + text: str + + @override + def __str__(self) -> str: + return f"Unable to parse JSON; got {self.text!r}" + + +@dataclass(kw_only=True, slots=True) +class _UvPipListBaseVersionError(UvPipListError): data: StrMapping @override @@ -1833,10 +1854,12 @@ def __str__(self) -> str: @dataclass(kw_only=True, slots=True) -class _UvPipListBaseError(UvPipListError): ... - +class _UvPipListOutdatedVersionError(UvPipListError): + data: StrMapping -class _UvPipListOutdatedError(UvPipListError): ... + @override + def __str__(self) -> str: + return f"Unable to parse version; got {self.data}" def uv_pip_list_cmd( diff --git a/uv.lock b/uv.lock index 14a0b6d80..2bd3bfb7f 100644 --- a/uv.lock +++ b/uv.lock @@ -625,7 +625,7 @@ wheels = [ [[package]] name = "dycw-utilities" -version = "0.184.2" +version = "0.184.3" source = { editable = "." } dependencies = [ { name = "atomicwrites" }, @@ -944,7 +944,7 @@ dev = [ { name = "coloredlogs", specifier = ">=15.0.1" }, { name = "coverage-conditional-plugin", specifier = ">=0.9.0" }, { name = "dycw-pytest-only", specifier = ">=2.1.1" }, - { name = "dycw-utilities", extras = ["test"], specifier = ">=0.184.1" }, + { name = "dycw-utilities", extras = ["test"], specifier = ">=0.184.2" }, { name = "pyright", specifier = ">=1.1.408" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-timeout", specifier = ">=2.4.0" },