diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e40119..645965d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ v1.3.0 (in development) ----------------------- - Support Python 3.14 - Drop support for Python 3.8 and 3.9 +- Drop `click` dependency v1.2.4 (2025-09-19) ------------------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 70aae20..7f808e3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,7 @@ v1.3.0 (in development) ----------------------- - Support Python 3.14 - Drop support for Python 3.8 and 3.9 +- Drop ``click`` dependency v1.2.4 (2025-09-19) diff --git a/pyproject.toml b/pyproject.toml index 706751a..85c18c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ classifiers = [ dependencies = [ "cachecontrol[filecache] >= 0.12, < 0.15", - "click >= 8.0, != 8.2.2", "platformdirs >= 2.1, < 5.0", "pydantic ~= 2.0", "requests ~= 2.20", @@ -67,9 +66,6 @@ include = [ "tox.ini", ] -[tool.hatch.envs.default] -python = "3" - [tool.mypy] allow_incomplete_defs = false allow_untyped_defs = false diff --git a/src/pyversion_info/__main__.py b/src/pyversion_info/__main__.py index 0c2b167..82870d6 100644 --- a/src/pyversion_info/__main__.py +++ b/src/pyversion_info/__main__.py @@ -1,10 +1,12 @@ from __future__ import annotations -from collections.abc import Callable +import argparse +from dataclasses import dataclass from datetime import date -from functools import partial, wraps +from enum import Enum +from functools import partial import json -from typing import Any -import click +import sys +from typing import Any, Protocol from . import ( CPythonVersionInfo, PyPyVersionInfo, @@ -16,199 +18,312 @@ from .util import MajorVersion, MicroVersion, MinorVersion -def map_exc_to_click(func: Callable) -> Callable: - @wraps(func) - def wrapped(*args: Any, **kwargs: Any) -> Any: +class Subcommand(Protocol): + def run(self, vd: VersionDatabase) -> int: ... + + +@dataclass +class Command: + database: str | None + subcommand: Subcommand + + @classmethod + def from_args(cls, argv: list[str] | None = None) -> Command: + parser = argparse.ArgumentParser( + description=( + "Show details about Python versions\n" + "\n" + "Visit for more information." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "-d", + "--database", + metavar="FILE|URL", + help="Fetch version information from the given database", + ) + parser.add_argument( + "-V", "--version", action="version", version=f"%(prog)s {__version__}" + ) + + subparsers = parser.add_subparsers(title="subcommands", dest="subcommand") + + listparser = subparsers.add_parser( + "list", help="List known versions at the given version level" + ) + listparser.add_argument( + "-a", + "--all", + dest="mode", + action="store_const", + const=Mode.ALL, + help="List all known versions", + # This goes on the first option with `dest="mode"`: + default=Mode.RELEASED, + ) + listparser.add_argument( + "--cpython", + dest="py", + action="store_const", + const=PyImpl.CPYTHON, + help="Show information about CPython versions [default]", + default=PyImpl.CPYTHON, + ) + listparser.add_argument( + "-n", + "--not-eol", + dest="mode", + action="store_const", + const=Mode.NOT_EOL, + help="List only versions that are not EOL (supported versions + unreleased versions)", + ) + listparser.add_argument( + "--pypy", + dest="py", + action="store_const", + const=PyImpl.PYPY, + help="Show information about PyPy versions", + ) + listparser.add_argument( + "-r", + "--released", + dest="mode", + action="store_const", + const=Mode.RELEASED, + help="List only released versions [default]", + ) + listparser.add_argument( + "-s", + "--supported", + dest="mode", + action="store_const", + const=Mode.SUPPORTED, + help="List only supported versions", + ) + # Don't convert with `type` here, as that takes effect before `choices` + listparser.add_argument("level", choices=["major", "minor", "micro"]) + + showparser = subparsers.add_parser( + "show", help="Show information about a Python version" + ) + showparser.add_argument( + "--cpython", + dest="py", + action="store_const", + const=PyImpl.CPYTHON, + help="Show information about CPython versions [default]", + default=PyImpl.CPYTHON, + ) + showparser.add_argument("-J", "--json", action="store_true", help="Output JSON") + showparser.add_argument( + "--pypy", + dest="py", + action="store_const", + const=PyImpl.PYPY, + help="Show information about PyPy versions", + ) + showparser.add_argument( + "-S", + "--subversions", + # Don't convert with `type` here, as that takes effect before `choices` + choices=["all", "not-eol", "released", "supported"], + help="Which subversions to list [default: released]", + default="released", + ) + showparser.add_argument("version") + + args = parser.parse_args(argv) + subcommand: Subcommand + match args.subcommand: + case "list": + subcommand = ListCommand( + level=Level(args.level), mode=args.mode, py=args.py + ) + case "show": + subcommand = ShowCommand( + version=args.version, + subversions=Mode(args.subversions), + json=args.json, + py=args.py, + ) + case cmd: # pragma: no cover + raise AssertionError(f"Unexpected subcommand: {cmd!r}") + return Command(database=args.database, subcommand=subcommand) + + def run(self) -> int: + if self.database is None: + vd = VersionDatabase.fetch() + elif self.database.lower().startswith(("http://", "https://")): + vd = VersionDatabase.fetch(self.database) + else: + vd = VersionDatabase.parse_file(self.database) try: - return func(*args, **kwargs) + return self.subcommand.run(vd) except ValueError as e: - raise click.UsageError(str(e)) + print(f"pyversion-info: {e}", file=sys.stderr) + return 1 - return wrapped +class PyImpl(Enum): + CPYTHON = 0 + PYPY = 1 -@click.group(context_settings={"help_option_names": ["-h", "--help"]}) -@click.version_option( - __version__, - "-V", - "--version", - message="%(prog)s %(version)s", -) -@click.option( - "-d", - "--database", - metavar="FILE|URL", - help="Fetch version information from the given database", -) -@click.pass_context -def main(ctx: click.Context, database: str | None) -> None: - """Show details about Python versions""" - if database is None: - ctx.obj = VersionDatabase.fetch() - elif database.lower().startswith(("http://", "https://")): - ctx.obj = VersionDatabase.fetch(database) - else: - ctx.obj = VersionDatabase.parse_file(database) - - -@main.command("list") -@click.option("-a", "--all", "mode", flag_value="all", help="List all known versions") -@click.option( - "--cpython", - "py", - flag_value="cpython", - help="Show information about CPython versions [default]", - default=True, -) -@click.option( - "-n", - "--not-eol", - "mode", - flag_value="not-eol", - help="List only versions that are not EOL (supported versions + unreleased versions)", -) -@click.option( - "--pypy", - "py", - flag_value="pypy", - help="Show information about PyPy versions", -) -@click.option( - "-r", - "--released", - "mode", - flag_value="released", - help="List only released versions [default]", -) -@click.option( - "-s", - "--supported", - "mode", - flag_value="supported", - help="List only supported versions", -) -@click.argument("level", type=click.Choice(["major", "minor", "micro"])) -@click.pass_obj -@map_exc_to_click -def list_cmd(vd: VersionDatabase, level: str, mode: str | None, py: str) -> None: - """List known versions at the given version level""" - if mode is None: - mode = "released" - info = vd.pypy if py == "pypy" else vd.cpython - func = { - "major": info.major_versions, - "minor": info.minor_versions, - "micro": info.micro_versions, - }[level] - for v in filter_versions(mode, info, func()): - print(v) - - -@main.command() -@click.option( - "--cpython", - "py", - flag_value="cpython", - help="Show information about CPython versions [default]", - default=True, -) -@click.option("-J", "--json", "do_json", is_flag=True, help="Output JSON") -@click.option( - "--pypy", - "py", - flag_value="pypy", - help="Show information about PyPy versions", -) -@click.option( - "-S", - "--subversions", - type=click.Choice(["all", "not-eol", "released", "supported"]), - help="Which subversions to list", - default="released", - show_default=True, -) -@click.argument("version") -@click.pass_obj -@map_exc_to_click -def show( - vd: VersionDatabase, version: str, subversions: str, do_json: bool, py: str -) -> None: - """Show information about a Python version""" - info = vd.pypy if py == "pypy" else vd.cpython - v = parse_version(version) - data: list[tuple[str, str, Any]] = [ - ("version", "Version", str(v)), - # ("level", "Level", ---), - ("release_date", "Release-Date", info.release_date(v)), - ("is_released", "Is-Released", info.is_released(v)), - ] - if isinstance(info, CPythonVersionInfo): - data.append(("is_supported", "Is-Supported", info.is_supported(v))) - if isinstance(v, MajorVersion): - data.insert(1, ("level", "Level", "major")) + def get_info(self, vd: VersionDatabase) -> VersionInfo: + match self: + case PyImpl.CPYTHON: + return vd.cpython + case PyImpl.PYPY: + return vd.pypy + case _: # pragma: no cover + raise AssertionError(f"Unexpected Python implementation: {self!r}") + + +class Mode(Enum): + ALL = "all" + NOT_EOL = "not-eol" + RELEASED = "released" + SUPPORTED = "supported" + + def filter_versions(self, info: VersionInfo, versions: list[str]) -> list[str]: + match self: + case Mode.ALL: + filterer = yes + case Mode.RELEASED: + filterer = info.is_released + case Mode.SUPPORTED: + if not isinstance(info, CPythonVersionInfo): + raise ValueError('"supported" only applies to CPython versions') + filterer = info.is_supported + case Mode.NOT_EOL: + if not isinstance(info, CPythonVersionInfo): + raise ValueError('"not-eol" only applies to CPython versions') + filterer = partial(is_not_eol, info) + case mode: # pragma: no cover + raise AssertionError(f"Unexpected mode: {mode!r}") + return list(filter(filterer, versions)) + + +class Level(Enum): + MAJOR = "major" + MINOR = "minor" + MICRO = "micro" + + def get_versions(self, info: VersionInfo) -> list[str]: + match self: + case Level.MAJOR: + return info.major_versions() + case Level.MINOR: + return info.minor_versions() + case Level.MICRO: + return info.micro_versions() + case level: # pragma: no cover + raise AssertionError(f"Unexpected level: {level!r}") + + +@dataclass +class ListCommand: + level: Level + mode: Mode + py: PyImpl + + def run(self, vd: VersionDatabase) -> int: + info = self.py.get_info(vd) + for v in self.mode.filter_versions(info, self.level.get_versions(info)): + print(v) + return 0 + + +@dataclass +class ShowCommand: + version: str + subversions: Mode + json: bool + py: PyImpl + + def run(self, vd: VersionDatabase) -> int: + info = self.py.get_info(vd) + v = parse_version(self.version) + data: list[tuple[str, str, Any]] = [ + ("version", "Version", str(v)), + # ("level", "Level", ---), + ("release_date", "Release-Date", info.release_date(v)), + ("is_released", "Is-Released", info.is_released(v)), + ] if isinstance(info, CPythonVersionInfo): - data.append(("eol_date", "EOL-Date", info.eol_date(v))) - data.append(("is_eol", "Is-EOL", info.is_eol(v))) - data.append( - ( - "subversions", - "Subversions", - filter_versions(subversions, info, info.subversions(v)), - ) - ) - if isinstance(info, PyPyVersionInfo): + data.append(("is_supported", "Is-Supported", info.is_supported(v))) + if isinstance(v, MajorVersion): + data.insert(1, ("level", "Level", "major")) + if isinstance(info, CPythonVersionInfo): + data.append(("eol_date", "EOL-Date", info.eol_date(v))) + data.append(("is_eol", "Is-EOL", info.is_eol(v))) data.append( ( - "cpython_series", - "CPython-Series", - info.supported_cpython_series( - v, released=subversions == "released" - ), + "subversions", + "Subversions", + self.subversions.filter_versions(info, info.subversions(v)), ) ) - elif isinstance(v, MinorVersion): - data.insert(1, ("level", "Level", "minor")) - if isinstance(info, CPythonVersionInfo): - data.append(("eol_date", "EOL-Date", info.eol_date(v))) - data.append(("is_eol", "Is-EOL", info.is_eol(v))) - data.append( - ( - "subversions", - "Subversions", - filter_versions(subversions, info, info.subversions(v)), - ) - ) - if isinstance(info, PyPyVersionInfo): + if isinstance(info, PyPyVersionInfo): + data.append( + ( + "cpython_series", + "CPython-Series", + info.supported_cpython_series( + v, + released=self.subversions is Mode.RELEASED, + ), + ) + ) + elif isinstance(v, MinorVersion): + data.insert(1, ("level", "Level", "minor")) + if isinstance(info, CPythonVersionInfo): + data.append(("eol_date", "EOL-Date", info.eol_date(v))) + data.append(("is_eol", "Is-EOL", info.is_eol(v))) data.append( ( - "cpython_series", - "CPython-Series", - info.supported_cpython_series( - v, released=subversions == "released" - ), + "subversions", + "Subversions", + self.subversions.filter_versions(info, info.subversions(v)), ) ) - else: - assert isinstance(v, MicroVersion) - data.insert(1, ("level", "Level", "micro")) - if isinstance(info, CPythonVersionInfo): - data.append(("eol_date", "EOL-Date", info.eol_date(v))) - data.append(("is_eol", "Is-EOL", info.is_eol(v))) - if isinstance(info, PyPyVersionInfo): - data.append(("cpython", "CPython", info.supported_cpython(v))) - if do_json: - print(json.dumps({k: v for k, _, v in data}, indent=4, default=str)) - else: - for _, label, val in data: - if isinstance(val, date): - val = str(val) - elif isinstance(val, bool): - val = "yes" if val else "no" - elif isinstance(val, list): - val = ", ".join(val) - elif val is None: - val = "UNKNOWN" - print(f"{label}: {val}") + if isinstance(info, PyPyVersionInfo): + data.append( + ( + "cpython_series", + "CPython-Series", + info.supported_cpython_series( + v, + released=self.subversions is Mode.RELEASED, + ), + ) + ) + else: + assert isinstance(v, MicroVersion) + data.insert(1, ("level", "Level", "micro")) + if isinstance(info, CPythonVersionInfo): + data.append(("eol_date", "EOL-Date", info.eol_date(v))) + data.append(("is_eol", "Is-EOL", info.is_eol(v))) + if isinstance(info, PyPyVersionInfo): + data.append(("cpython", "CPython", info.supported_cpython(v))) + if self.json: + print(json.dumps({k: v for k, _, v in data}, indent=4, default=str)) + else: + for _, label, val in data: + if isinstance(val, date): + val = str(val) + elif isinstance(val, bool): + val = "yes" if val else "no" + elif isinstance(val, list): + val = ", ".join(val) + elif val is None: + val = "UNKNOWN" + print(f"{label}: {val}") + return 0 + + +def main(argv: list[str] | None = None) -> int: + return Command.from_args(argv).run() def is_not_eol(pyvinfo: CPythonVersionInfo, version: str) -> bool: @@ -219,23 +334,5 @@ def yes(version: str) -> bool: # noqa: U100 return True -def filter_versions(mode: str, info: VersionInfo, versions: list[str]) -> list[str]: - if mode == "all": - filterer = yes - elif mode == "released": - filterer = info.is_released - elif mode == "supported": - if not isinstance(info, CPythonVersionInfo): - raise click.UsageError("'supported' only applies to CPython versions") - filterer = info.is_supported - elif mode == "not-eol": - if not isinstance(info, CPythonVersionInfo): - raise click.UsageError("'not-eol' only applies to CPython versions") - filterer = partial(is_not_eol, info) - else: - raise AssertionError(f"Unexpected mode: {mode!r}") # pragma: no cover - return list(filter(filterer, versions)) - - if __name__ == "__main__": - main() # pragma: no cover + sys.exit(main()) # pragma: no cover diff --git a/test/test_cmd_cpython.py b/test/test_cmd_cpython.py index bed4f08..0207ccf 100644 --- a/test/test_cmd_cpython.py +++ b/test/test_cmd_cpython.py @@ -1,9 +1,6 @@ from __future__ import annotations import json from pathlib import Path -from traceback import format_exception -import click -from click.testing import CliRunner, Result import pytest from pytest_mock import MockerFixture from pyversion_info.__main__ import main @@ -22,14 +19,6 @@ def use_fixed_date(mocker: MockerFixture) -> None: # Time is now 2019-04-23T16:46:48-04:00. -def show_result(r: Result) -> str: - if r.exception is not None: - assert isinstance(r.exc_info, tuple) - return "".join(format_exception(*r.exc_info)) - else: - return r.output - - @pytest.mark.parametrize( "mode,versions", [ @@ -39,14 +28,18 @@ def show_result(r: Result) -> str: ("--supported", ["2", "3"]), ], ) -def test_cmd_list_major(mode: str, versions: list[str]) -> None: - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", mode, "major"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) +def test_cmd_list_major( + capsys: pytest.CaptureFixture[str], mode: str, versions: list[str] +) -> None: + assert main(["-d", DATA_FILE, "list", mode, "major"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" if mode == "--released": - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", "major"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) + assert main(["-d", DATA_FILE, "list", "major"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" @pytest.mark.parametrize( @@ -116,14 +109,17 @@ def test_cmd_list_major(mode: str, versions: list[str]) -> None: ("--supported", ["2.7", "3.5", "3.6", "3.7"]), ], ) -def test_cmd_list_minor(mode: str, versions: list[str]) -> None: - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", mode, "minor"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) +def test_cmd_list_minor( + capsys: pytest.CaptureFixture[str], mode: str, versions: list[str] +) -> None: + assert main(["-d", DATA_FILE, "list", mode, "minor"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" if mode == "--released": - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", "minor"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) + assert main(["-d", DATA_FILE, "list", "minor"]) == 0 + assert out == "".join(v + "\n" for v in versions) + assert err == "" @pytest.mark.parametrize( @@ -511,14 +507,18 @@ def test_cmd_list_minor(mode: str, versions: list[str]) -> None: ), ], ) -def test_cmd_list_micro(mode: str, versions: list[str]) -> None: - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", mode, "micro"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) +def test_cmd_list_micro( + capsys: pytest.CaptureFixture[str], mode: str, versions: list[str] +) -> None: + assert main(["-d", DATA_FILE, "list", mode, "micro"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" if mode == "--released": - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", "micro"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) + assert main(["-d", DATA_FILE, "list", "micro"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" @pytest.mark.parametrize( @@ -1116,21 +1116,29 @@ def test_cmd_list_micro(mode: str, versions: list[str]) -> None: ), ], ) -def test_show(version: str, subversions: str, data: dict, headers: str) -> None: - r = CliRunner().invoke( - main, ["-d", DATA_FILE, "show", "--subversions", subversions, version] - ) - assert r.exit_code == 0, show_result(r) - assert r.output == headers +def test_show( + capsys: pytest.CaptureFixture[str], + version: str, + subversions: str, + data: dict, + headers: str, +) -> None: + assert main(["-d", DATA_FILE, "show", "--subversions", subversions, version]) == 0 + out, err = capsys.readouterr() + assert out == headers + assert err == "" if subversions == "released": - r = CliRunner().invoke(main, ["-d", DATA_FILE, "show", version]) - assert r.exit_code == 0, show_result(r) - assert r.output == headers - r = CliRunner().invoke( - main, ["-d", DATA_FILE, "show", "--json", "--subversions", subversions, version] + assert main(["-d", DATA_FILE, "show", version]) == 0 + out, err = capsys.readouterr() + assert out == headers + assert err == "" + assert ( + main(["-d", DATA_FILE, "show", "--json", "--subversions", subversions, version]) + == 0 ) - assert r.exit_code == 0, show_result(r) - assert json.loads(r.output) == data + out, err = capsys.readouterr() + assert json.loads(out) == data + assert err == "" @pytest.mark.parametrize( @@ -1223,24 +1231,31 @@ def test_show(version: str, subversions: str, data: dict, headers: str) -> None: ], ) @pytest.mark.parametrize("subversions", ["released", "all", "supported", "not-eol"]) -def test_show_micro(version: str, subversions: str, data: dict, headers: str) -> None: - r = CliRunner().invoke( - main, ["-d", DATA_FILE, "show", "--subversions", subversions, version] - ) - assert r.exit_code == 0, show_result(r) - assert r.output == headers - r = CliRunner().invoke( - main, ["-d", DATA_FILE, "show", "--json", "--subversions", subversions, version] +def test_show_micro( + capsys: pytest.CaptureFixture[str], + version: str, + subversions: str, + data: dict, + headers: str, +) -> None: + assert main(["-d", DATA_FILE, "show", "--subversions", subversions, version]) == 0 + out, err = capsys.readouterr() + assert out == headers + assert err == "" + assert ( + main(["-d", DATA_FILE, "show", "--json", "--subversions", subversions, version]) + == 0 ) - assert r.exit_code == 0, show_result(r) - assert json.loads(r.output) == data + out, err = capsys.readouterr() + assert json.loads(out) == data + assert err == "" -def test_show_recent(mocker: MockerFixture) -> None: +def test_show_recent(capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None: mocker.patch("time.time", return_value=1635992101) - r = CliRunner().invoke(main, ["-d", DATA_FILE, "show", "2"]) - assert r.exit_code == 0, show_result(r) - assert r.output == ( + assert main(["-d", DATA_FILE, "show", "2"]) == 0 + out, err = capsys.readouterr() + assert out == ( "Version: 2\n" "Level: major\n" "Release-Date: 2000-10-16\n" @@ -1250,9 +1265,10 @@ def test_show_recent(mocker: MockerFixture) -> None: "Is-EOL: yes\n" "Subversions: 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7\n" ) - r = CliRunner().invoke(main, ["-d", DATA_FILE, "show", "--json", "2"]) - assert r.exit_code == 0, show_result(r) - assert json.loads(r.output) == { + assert err == "" + assert main(["-d", DATA_FILE, "show", "--json", "2"]) == 0 + out, err = capsys.readouterr() + assert json.loads(out) == { "version": "2", "level": "major", "release_date": "2000-10-16", @@ -1262,19 +1278,20 @@ def test_show_recent(mocker: MockerFixture) -> None: "is_eol": True, "subversions": ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6", "2.7"], } + assert err == "" @pytest.mark.parametrize("v", ["", "1.2.3.4", "1.2.3rc1", "foobar", "a.b.c"]) -def test_show_invalid_version(v: str) -> None: - r = CliRunner().invoke(main, ["-d", DATA_FILE, "show", v], standalone_mode=False) - assert r.exit_code != 0 - assert isinstance(r.exception, click.UsageError) - assert str(r.exception) == f"Invalid version string: {v!r}" +def test_show_invalid_version(capsys: pytest.CaptureFixture[str], v: str) -> None: + assert main(["-d", DATA_FILE, "show", v]) == 1 + out, err = capsys.readouterr() + assert out == "" + assert err == f"pyversion-info: Invalid version string: {v!r}\n" @pytest.mark.parametrize("v", ["0.8", "2.5.7", "3.9", "3.9.0", "5"]) -def test_show_unknown_version(v: str) -> None: - r = CliRunner().invoke(main, ["-d", DATA_FILE, "show", v], standalone_mode=False) - assert r.exit_code != 0 - assert isinstance(r.exception, click.UsageError) - assert str(r.exception) == f"Unknown version: {v!r}" +def test_show_unknown_version(capsys: pytest.CaptureFixture[str], v: str) -> None: + assert main(["-d", DATA_FILE, "show", v]) == 1 + out, err = capsys.readouterr() + assert out == "" + assert err == f"pyversion-info: Unknown version: {v!r}\n" diff --git a/test/test_cmd_pypy.py b/test/test_cmd_pypy.py index 63bd620..06e218d 100644 --- a/test/test_cmd_pypy.py +++ b/test/test_cmd_pypy.py @@ -1,9 +1,6 @@ from __future__ import annotations import json from pathlib import Path -from traceback import format_exception -import click -from click.testing import CliRunner, Result import pytest from pytest_mock import MockerFixture from pyversion_info.__main__ import main @@ -17,14 +14,6 @@ def use_fixed_date(mocker: MockerFixture) -> None: # Time is now 2021-11-04T02:15:01+00:00. -def show_result(r: Result) -> str: - if r.exception is not None: - assert isinstance(r.exc_info, tuple) - return "".join(format_exception(*r.exc_info)) - else: - return r.output - - @pytest.mark.parametrize( "mode,versions", [ @@ -32,14 +21,18 @@ def show_result(r: Result) -> str: ("--released", ["1", "2", "4", "5", "6", "7"]), ], ) -def test_cmd_list_major(mode: str, versions: list[str]) -> None: - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", "--pypy", mode, "major"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) +def test_cmd_list_major( + capsys: pytest.CaptureFixture[str], mode: str, versions: list[str] +) -> None: + assert main(["-d", DATA_FILE, "list", "--pypy", mode, "major"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" if mode == "--released": - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", "--pypy", "major"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) + assert main(["-d", DATA_FILE, "list", "--pypy", "major"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" RELEASED_MINOR = [ @@ -79,14 +72,18 @@ def test_cmd_list_major(mode: str, versions: list[str]) -> None: ("--released", RELEASED_MINOR), ], ) -def test_cmd_list_minor(mode: str, versions: list[str]) -> None: - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", "--pypy", mode, "minor"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) +def test_cmd_list_minor( + capsys: pytest.CaptureFixture[str], mode: str, versions: list[str] +) -> None: + assert main(["-d", DATA_FILE, "list", "--pypy", mode, "minor"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" if mode == "--released": - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", "--pypy", "minor"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) + assert main(["-d", DATA_FILE, "list", "--pypy", "minor"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" RELEASED_MICRO = [ @@ -148,38 +145,44 @@ def test_cmd_list_minor(mode: str, versions: list[str]) -> None: ("--released", RELEASED_MICRO), ], ) -def test_cmd_list_micro(mode: str, versions: list[str]) -> None: - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", "--pypy", mode, "micro"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) +def test_cmd_list_micro( + capsys: pytest.CaptureFixture[str], mode: str, versions: list[str] +) -> None: + assert main(["-d", DATA_FILE, "list", "--pypy", mode, "micro"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" if mode == "--released": - r = CliRunner().invoke(main, ["-d", DATA_FILE, "list", "--pypy", "micro"]) - assert r.exit_code == 0, show_result(r) - assert r.output == "".join(v + "\n" for v in versions) + assert main(["-d", DATA_FILE, "list", "--pypy", "micro"]) == 0 + out, err = capsys.readouterr() + assert out == "".join(v + "\n" for v in versions) + assert err == "" @pytest.mark.parametrize("level", ["major", "minor", "micro"]) -def test_cmd_list_not_eol(level: str) -> None: - r = CliRunner().invoke( - main, - ["-d", DATA_FILE, "list", "--pypy", "--not-eol", level], - standalone_mode=False, +def test_cmd_list_not_eol(capsys: pytest.CaptureFixture[str], level: str) -> None: + assert ( + main( + ["-d", DATA_FILE, "list", "--pypy", "--not-eol", level], + ) + == 1 ) - assert r.exit_code != 0 - assert isinstance(r.exception, click.UsageError) - assert str(r.exception) == "'not-eol' only applies to CPython versions" + out, err = capsys.readouterr() + assert out == "" + assert err == 'pyversion-info: "not-eol" only applies to CPython versions\n' @pytest.mark.parametrize("level", ["major", "minor", "micro"]) -def test_cmd_list_supported(level: str) -> None: - r = CliRunner().invoke( - main, - ["-d", DATA_FILE, "list", "--pypy", "--supported", level], - standalone_mode=False, +def test_cmd_list_supported(capsys: pytest.CaptureFixture[str], level: str) -> None: + assert ( + main( + ["-d", DATA_FILE, "list", "--pypy", "--supported", level], + ) + == 1 ) - assert r.exit_code != 0 - assert isinstance(r.exception, click.UsageError) - assert str(r.exception) == "'supported' only applies to CPython versions" + out, err = capsys.readouterr() + assert out == "" + assert err == 'pyversion-info: "supported" only applies to CPython versions\n' @pytest.mark.parametrize( @@ -279,27 +282,38 @@ def test_cmd_list_supported(level: str) -> None: ], ) @pytest.mark.parametrize("subversions", ["released", "all"]) -def test_show(version: str, subversions: str, data: dict, headers: str) -> None: - r = CliRunner().invoke( - main, ["-d", DATA_FILE, "show", "--pypy", "--subversions", subversions, version] +def test_show( + capsys: pytest.CaptureFixture[str], + version: str, + subversions: str, + data: dict, + headers: str, +) -> None: + assert ( + main(["-d", DATA_FILE, "show", "--pypy", "--subversions", subversions, version]) + == 0 ) - assert r.exit_code == 0, show_result(r) - assert r.output == headers - r = CliRunner().invoke( - main, - [ - "-d", - DATA_FILE, - "show", - "--pypy", - "--json", - "--subversions", - subversions, - version, - ], + out, err = capsys.readouterr() + assert out == headers + assert err == "" + assert ( + main( + [ + "-d", + DATA_FILE, + "show", + "--pypy", + "--json", + "--subversions", + subversions, + version, + ] + ) + == 0 ) - assert r.exit_code == 0, show_result(r) - assert json.loads(r.output) == data + out, err = capsys.readouterr() + assert json.loads(out) == data + assert err == "" @pytest.mark.parametrize( @@ -388,73 +402,81 @@ def test_show(version: str, subversions: str, data: dict, headers: str) -> None: ], ) def test_show_not_all_released( - version: str, subversions: str, data: dict, headers: str + capsys: pytest.CaptureFixture[str], + version: str, + subversions: str, + data: dict, + headers: str, ) -> None: - r = CliRunner().invoke( - main, ["-d", DATA_FILE, "show", "--pypy", "--subversions", subversions, version] + assert ( + main(["-d", DATA_FILE, "show", "--pypy", "--subversions", subversions, version]) + == 0 ) - assert r.exit_code == 0, show_result(r) - assert r.output == headers + out, err = capsys.readouterr() + assert out == headers + assert err == "" if subversions == "released": - r = CliRunner().invoke(main, ["-d", DATA_FILE, "show", "--pypy", version]) - assert r.exit_code == 0, show_result(r) - assert r.output == headers - r = CliRunner().invoke( - main, - [ - "-d", - DATA_FILE, - "show", - "--pypy", - "--json", - "--subversions", - subversions, - version, - ], + assert main(["-d", DATA_FILE, "show", "--pypy", version]) == 0 + out, err = capsys.readouterr() + assert out == headers + assert err == "" + assert ( + main( + [ + "-d", + DATA_FILE, + "show", + "--pypy", + "--json", + "--subversions", + subversions, + version, + ] + ) + == 0 ) - assert r.exit_code == 0, show_result(r) - assert json.loads(r.output) == data + out, err = capsys.readouterr() + assert json.loads(out) == data + assert err == "" @pytest.mark.parametrize("v", ["7", "7.3"]) -def test_cmd_show_not_eol(v: str) -> None: - r = CliRunner().invoke( - main, - ["-d", DATA_FILE, "show", "--pypy", "--subversions", "not-eol", v], - standalone_mode=False, +def test_cmd_show_not_eol(capsys: pytest.CaptureFixture[str], v: str) -> None: + assert ( + main( + ["-d", DATA_FILE, "show", "--pypy", "--subversions", "not-eol", v], + ) + == 1 ) - assert r.exit_code != 0 - assert isinstance(r.exception, click.UsageError) - assert str(r.exception) == "'not-eol' only applies to CPython versions" + out, err = capsys.readouterr() + assert out == "" + assert err == 'pyversion-info: "not-eol" only applies to CPython versions\n' @pytest.mark.parametrize("v", ["7", "7.3"]) -def test_cmd_show_supported(v: str) -> None: - r = CliRunner().invoke( - main, - ["-d", DATA_FILE, "show", "--pypy", "--subversions", "supported", v], - standalone_mode=False, +def test_cmd_show_supported(capsys: pytest.CaptureFixture[str], v: str) -> None: + assert ( + main( + ["-d", DATA_FILE, "show", "--pypy", "--subversions", "supported", v], + ) + == 1 ) - assert r.exit_code != 0 - assert isinstance(r.exception, click.UsageError) - assert str(r.exception) == "'supported' only applies to CPython versions" + out, err = capsys.readouterr() + assert out == "" + assert err == 'pyversion-info: "supported" only applies to CPython versions\n' @pytest.mark.parametrize("v", ["", "1.2.3.4", "1.2.3rc1", "foobar", "a.b.c"]) -def test_show_invalid_version(v: str) -> None: - r = CliRunner().invoke( - main, ["-d", DATA_FILE, "show", "--pypy", v], standalone_mode=False - ) - assert r.exit_code != 0 - assert isinstance(r.exception, click.UsageError) - assert str(r.exception) == f"Invalid version string: {v!r}" +def test_show_invalid_version(capsys: pytest.CaptureFixture[str], v: str) -> None: + assert main(["-d", DATA_FILE, "show", "--pypy", v]) == 1 + out, err = capsys.readouterr() + assert out == "" + assert err == f"pyversion-info: Invalid version string: {v!r}\n" @pytest.mark.parametrize("v", ["0.8", "1.5", "3", "7.3.9", "9.0.0"]) -def test_show_unknown_version(v: str) -> None: - r = CliRunner().invoke( - main, ["-d", DATA_FILE, "show", "--pypy", v], standalone_mode=False - ) - assert r.exit_code != 0 - assert isinstance(r.exception, click.UsageError) - assert str(r.exception) == f"Unknown version: {v!r}" +def test_show_unknown_version(capsys: pytest.CaptureFixture[str], v: str) -> None: + assert main(["-d", DATA_FILE, "show", "--pypy", v]) == 1 + out, err = capsys.readouterr() + assert out == "" + assert err == f"pyversion-info: Unknown version: {v!r}\n"