|
| 1 | +from datetime import date |
| 2 | +from functools import partial, wraps |
| 3 | +import json |
| 4 | +import re |
| 5 | +from typing import Any, Callable, Dict, Iterator, List, Optional |
| 6 | +import click |
| 7 | +from . import ( |
| 8 | + PyVersionInfo, |
| 9 | + __version__, |
| 10 | + get_pyversion_info, |
| 11 | + parse_version, |
| 12 | + unparse_version, |
| 13 | +) |
| 14 | + |
| 15 | + |
| 16 | +def map_exc_to_click(func: Callable) -> Callable: |
| 17 | + @wraps(func) |
| 18 | + def wrapped(*args: Any, **kwargs: Any) -> Any: |
| 19 | + try: |
| 20 | + return func(*args, **kwargs) |
| 21 | + except ValueError as e: |
| 22 | + raise click.UsageError(str(e)) |
| 23 | + |
| 24 | + return wrapped |
| 25 | + |
| 26 | + |
| 27 | +@click.group() |
| 28 | +@click.version_option( |
| 29 | + __version__, |
| 30 | + "-V", |
| 31 | + "--version", |
| 32 | + message="%(prog)s %(version)s", |
| 33 | +) |
| 34 | +@click.option( |
| 35 | + "-d", |
| 36 | + "--database", |
| 37 | + metavar="FILE|URL", |
| 38 | + help="Fetch version information from the given database", |
| 39 | +) |
| 40 | +@click.pass_context |
| 41 | +def main(ctx: click.Context, database: Optional[str]) -> None: |
| 42 | + """Show details about Python versions""" |
| 43 | + if database is None: |
| 44 | + ctx.obj = get_pyversion_info() |
| 45 | + elif database.lower().startswith(("http://", "https://")): |
| 46 | + ctx.obj = get_pyversion_info(database) |
| 47 | + else: |
| 48 | + with open(database, "rb") as fp: |
| 49 | + ctx.obj = PyVersionInfo(json.load(fp)) |
| 50 | + |
| 51 | + |
| 52 | +@main.command("list") |
| 53 | +@click.option("-a", "--all", "mode", flag_value="all", help="List all known versions") |
| 54 | +@click.option( |
| 55 | + "-n", |
| 56 | + "--not-eol", |
| 57 | + "mode", |
| 58 | + flag_value="not-eol", |
| 59 | + help="List only versions that are not EOL (supported versions + unreleased versions)", |
| 60 | +) |
| 61 | +@click.option( |
| 62 | + "-r", |
| 63 | + "--released", |
| 64 | + "mode", |
| 65 | + flag_value="released", |
| 66 | + help="List only released versions [default]", |
| 67 | + default=True, |
| 68 | +) |
| 69 | +@click.option( |
| 70 | + "-s", |
| 71 | + "--supported", |
| 72 | + "mode", |
| 73 | + flag_value="supported", |
| 74 | + help="List only supported versions", |
| 75 | +) |
| 76 | +@click.argument("level", type=click.Choice(["major", "minor", "micro"])) |
| 77 | +@click.pass_obj |
| 78 | +@map_exc_to_click |
| 79 | +def list_cmd(pyvinfo: PyVersionInfo, level: str, mode: str) -> None: |
| 80 | + """List known versions at the given version level""" |
| 81 | + func = { |
| 82 | + "major": pyvinfo.major_versions, |
| 83 | + "minor": pyvinfo.minor_versions, |
| 84 | + "micro": pyvinfo.micro_versions, |
| 85 | + }[level] |
| 86 | + for v in filter_versions(mode, pyvinfo, func): |
| 87 | + print(v) |
| 88 | + |
| 89 | + |
| 90 | +@main.command() |
| 91 | +@click.option("-J", "--json", "do_json", is_flag=True, help="Output JSON") |
| 92 | +@click.option( |
| 93 | + "-S", |
| 94 | + "--subversions", |
| 95 | + type=click.Choice(["all", "not-eol", "released", "supported"]), |
| 96 | + help="Which subversions to list", |
| 97 | + default="released", |
| 98 | + show_default=True, |
| 99 | +) |
| 100 | +@click.argument("version") |
| 101 | +@click.pass_obj |
| 102 | +@map_exc_to_click |
| 103 | +def show(pyvinfo: PyVersionInfo, version: str, subversions: str, do_json: bool) -> None: |
| 104 | + """Show information about a Python version""" |
| 105 | + v = parse_version(version) |
| 106 | + data: Dict[str, Any] = { |
| 107 | + "version": unparse_version(v), |
| 108 | + "level": None, # So it will show up on line 2 |
| 109 | + "release_date": pyvinfo.release_date(version), |
| 110 | + "is_released": pyvinfo.is_released(version), |
| 111 | + "is_supported": pyvinfo.is_supported(version), |
| 112 | + } |
| 113 | + if len(v) == 1: |
| 114 | + data["level"] = "major" |
| 115 | + data["subversions"] = list( |
| 116 | + filter_versions(subversions, pyvinfo, partial(pyvinfo.subversions, version)) |
| 117 | + ) |
| 118 | + elif len(v) == 2: |
| 119 | + data["level"] = "minor" |
| 120 | + data["eol_date"] = pyvinfo.eol_date(version) |
| 121 | + data["is_eol"] = pyvinfo.is_eol(version) |
| 122 | + data["subversions"] = list( |
| 123 | + filter_versions(subversions, pyvinfo, partial(pyvinfo.subversions, version)) |
| 124 | + ) |
| 125 | + else: |
| 126 | + data["level"] = "micro" |
| 127 | + if do_json: |
| 128 | + print(json.dumps(data, indent=4, default=str)) |
| 129 | + else: |
| 130 | + for k, val in data.items(): |
| 131 | + label = re.sub(r"[Ee]ol", "EOL", k.replace("_", "-").capitalize()) |
| 132 | + if k in ("release_date", "eol_date"): |
| 133 | + if isinstance(val, date): |
| 134 | + val = str(val) |
| 135 | + else: |
| 136 | + val = "UNKNOWN" |
| 137 | + elif isinstance(val, bool): |
| 138 | + val = "yes" if val else "no" |
| 139 | + elif isinstance(val, list): |
| 140 | + val = ", ".join(val) |
| 141 | + print(f"{label}: {val}") |
| 142 | + |
| 143 | + |
| 144 | +def is_not_eol(pyvinfo: PyVersionInfo, version: str) -> bool: |
| 145 | + v = parse_version(version) |
| 146 | + if len(v) == 1: |
| 147 | + return not all( |
| 148 | + map(pyvinfo.is_eol, pyvinfo.subversions(version, unreleased=True)) |
| 149 | + ) |
| 150 | + elif len(v) == 2: |
| 151 | + return not pyvinfo.is_eol(version) |
| 152 | + else: |
| 153 | + x, y, _ = v |
| 154 | + return not pyvinfo.is_eol(unparse_version((x, y))) |
| 155 | + |
| 156 | + |
| 157 | +def yes(version: str) -> bool: # noqa: U100 |
| 158 | + return True |
| 159 | + |
| 160 | + |
| 161 | +def filter_versions( |
| 162 | + mode: str, pyvinfo: PyVersionInfo, vfunc: Callable[[bool], List[str]] |
| 163 | +) -> Iterator[str]: |
| 164 | + if mode == "all": |
| 165 | + filterer = yes |
| 166 | + unreleased = True |
| 167 | + elif mode == "released": |
| 168 | + filterer = yes |
| 169 | + unreleased = False |
| 170 | + elif mode == "supported": |
| 171 | + filterer = pyvinfo.is_supported |
| 172 | + unreleased = False |
| 173 | + elif mode == "not-eol": |
| 174 | + filterer = partial(is_not_eol, pyvinfo) |
| 175 | + unreleased = True |
| 176 | + else: |
| 177 | + raise AssertionError(f"Unexpected mode: {mode!r}") # pragma: no cover |
| 178 | + return filter(filterer, vfunc(unreleased)) |
| 179 | + |
| 180 | + |
| 181 | +if __name__ == "__main__": |
| 182 | + main() # pragma: no cover |
0 commit comments