hassle.utilities

  1import re
  2
  3import coverage
  4import packagelister
  5import pytest
  6import requests
  7from bs4 import BeautifulSoup
  8from gitbetter import Git
  9from pathier import Pathier, Pathish
 10
 11root = Pathier(__file__).parent
 12
 13
 14def swap_keys(data: dict, keys: tuple[str, str]):
 15    """Convert between keys in `data`.
 16    The order of `keys` doesn't matter.
 17    >>> data = {"one two": 1}
 18    >>> data = swap_keys(data, ("one two", "one-two"))
 19    >>> print(data)
 20    >>> {"one-two": 1}
 21    >>> data = swap_keys(data, ("one two", "one-two"))
 22    >>> print(data)
 23    >>> {"one two": 1}
 24    """
 25    key1, key2 = keys
 26    data_keys = data.keys()
 27    if key1 in data_keys:
 28        data[key2] = data.pop(key1)
 29    elif key2 in data_keys:
 30        data[key1] = data.pop(key2)
 31    return data
 32
 33
 34def run_tests() -> bool:
 35    """Invoke `coverage` and `pytest -s`.
 36
 37    Returns `True` if all tests passed or if no tests were found."""
 38    cover = coverage.Coverage()
 39    cover.start()
 40    results = pytest.main(["-s"])
 41    cover.stop()
 42    cover.report()
 43    return results in [0, 5]
 44
 45
 46def check_pypi(package_name: str) -> bool:
 47    """Check if a package with package_name already exists on `pypi.org`.
 48    Returns `True` if package name exists.
 49    Only checks the first page of results."""
 50    url = f"https://pypi.org/search/?q={package_name.lower()}"
 51    response = requests.get(url)
 52    if response.status_code != 200:
 53        raise RuntimeError(
 54            f"Error: pypi.org returned status code: {response.status_code}"
 55        )
 56    soup = BeautifulSoup(response.text, "html.parser")
 57    pypi_packages = [
 58        span.text.lower()
 59        for span in soup.find_all("span", class_="package-snippet__name")
 60    ]
 61    return package_name in pypi_packages
 62
 63
 64def get_answer(question: str) -> bool | None:
 65    """Repeatedly ask the user a yes/no question until a 'y' or a 'n' is received."""
 66    ans = ""
 67    question = question.strip()
 68    if "?" not in question:
 69        question += "?"
 70    question += " (y/n): "
 71    while ans not in ["y", "yes", "no", "n"]:
 72        ans = input(question).strip().lower()
 73        if ans in ["y", "yes"]:
 74            return True
 75        elif ans in ["n", "no"]:
 76            return False
 77        else:
 78            print("Invalid answer.")
 79
 80
 81def bump_version(current_version: str, bump_type: str) -> str:
 82    """Bump `current_version` according to `bump_type` and return the new version.
 83
 84    #### :params:
 85
 86    `current_version`: A version string conforming to Semantic Versioning standards.
 87    i.e. `{major}.{minor}.{patch}`
 88
 89    `bump_type` can be one of `major`, `minor`, or `patch`.
 90
 91    Raises an exception if `current_version` is formatted incorrectly or if `bump_type` isn't one of the aforementioned types.
 92    """
 93    if not re.findall(r"[0-9]+.[0-9]+.[0-9]+", current_version):
 94        raise ValueError(
 95            f"{current_version} does not appear to match the required format of `x.x.x`."
 96        )
 97    bump_type = bump_type.lower().strip()
 98    if bump_type not in ["major", "minor", "patch"]:
 99        raise ValueError(
100            f"`bump_type` {bump_type} is not one of `major`, `minor`, or `patch`."
101        )
102    major, minor, patch = [int(part) for part in current_version.split(".")]
103    if bump_type == "major":
104        major += 1
105        minor = 0
106        patch = 0
107    elif bump_type == "minor":
108        minor += 1
109        patch = 0
110    elif bump_type == "patch":
111        patch += 1
112    return f"{major}.{minor}.{patch}"
113
114
115def get_dependencies(scandir: Pathish) -> list[tuple[str, str | None]]:
116    """Scan `scandir` and return a list of tuples like `(package, version)`.
117
118    The version may be `None`."""
119    packages = packagelister.scan(scandir)
120    # Replace package names for packages known to have a different pip install name than import name
121    swaps = (root / "package_name_swaps.toml").loads()
122    for key in swaps:
123        packages = swap_keys(packages, (key, swaps[key]))
124    dependencies = []
125    for package in packages:
126        dependencies.append((package, packages[package].get("version")))
127    return dependencies
128
129
130def format_dependency(dependency: tuple[str, str | None], include_version: bool) -> str:
131    """Format a dependency into a string.
132
133    If `include_version` and `dependency[1] is not None`, the return format will be `{package}~={version}`.
134
135    Otherwise, just `{package}`."""
136    if include_version and dependency[1]:
137        return f"{dependency[0]}~={dependency[1]}"
138    else:
139        return dependency[0]
140
141
142def on_primary_branch() -> bool:
143    """Returns `False` if repo is not currently on `main` or `master` branch."""
144    git = Git(True)
145    if git.current_branch not in ["main", "master"]:
146        return False
147    return True
def swap_keys(data: dict, keys: tuple[str, str]):
15def swap_keys(data: dict, keys: tuple[str, str]):
16    """Convert between keys in `data`.
17    The order of `keys` doesn't matter.
18    >>> data = {"one two": 1}
19    >>> data = swap_keys(data, ("one two", "one-two"))
20    >>> print(data)
21    >>> {"one-two": 1}
22    >>> data = swap_keys(data, ("one two", "one-two"))
23    >>> print(data)
24    >>> {"one two": 1}
25    """
26    key1, key2 = keys
27    data_keys = data.keys()
28    if key1 in data_keys:
29        data[key2] = data.pop(key1)
30    elif key2 in data_keys:
31        data[key1] = data.pop(key2)
32    return data

Convert between keys in data. The order of keys doesn't matter.

>>> data = {"one two": 1}
>>> data = swap_keys(data, ("one two", "one-two"))
>>> print(data)
>>> {"one-two": 1}
>>> data = swap_keys(data, ("one two", "one-two"))
>>> print(data)
>>> {"one two": 1}
def run_tests() -> bool:
35def run_tests() -> bool:
36    """Invoke `coverage` and `pytest -s`.
37
38    Returns `True` if all tests passed or if no tests were found."""
39    cover = coverage.Coverage()
40    cover.start()
41    results = pytest.main(["-s"])
42    cover.stop()
43    cover.report()
44    return results in [0, 5]

Invoke coverage and pytest -s.

Returns True if all tests passed or if no tests were found.

def check_pypi(package_name: str) -> bool:
47def check_pypi(package_name: str) -> bool:
48    """Check if a package with package_name already exists on `pypi.org`.
49    Returns `True` if package name exists.
50    Only checks the first page of results."""
51    url = f"https://pypi.org/search/?q={package_name.lower()}"
52    response = requests.get(url)
53    if response.status_code != 200:
54        raise RuntimeError(
55            f"Error: pypi.org returned status code: {response.status_code}"
56        )
57    soup = BeautifulSoup(response.text, "html.parser")
58    pypi_packages = [
59        span.text.lower()
60        for span in soup.find_all("span", class_="package-snippet__name")
61    ]
62    return package_name in pypi_packages

Check if a package with package_name already exists on pypi.org. Returns True if package name exists. Only checks the first page of results.

def get_answer(question: str) -> bool | None:
65def get_answer(question: str) -> bool | None:
66    """Repeatedly ask the user a yes/no question until a 'y' or a 'n' is received."""
67    ans = ""
68    question = question.strip()
69    if "?" not in question:
70        question += "?"
71    question += " (y/n): "
72    while ans not in ["y", "yes", "no", "n"]:
73        ans = input(question).strip().lower()
74        if ans in ["y", "yes"]:
75            return True
76        elif ans in ["n", "no"]:
77            return False
78        else:
79            print("Invalid answer.")

Repeatedly ask the user a yes/no question until a 'y' or a 'n' is received.

def bump_version(current_version: str, bump_type: str) -> str:
 82def bump_version(current_version: str, bump_type: str) -> str:
 83    """Bump `current_version` according to `bump_type` and return the new version.
 84
 85    #### :params:
 86
 87    `current_version`: A version string conforming to Semantic Versioning standards.
 88    i.e. `{major}.{minor}.{patch}`
 89
 90    `bump_type` can be one of `major`, `minor`, or `patch`.
 91
 92    Raises an exception if `current_version` is formatted incorrectly or if `bump_type` isn't one of the aforementioned types.
 93    """
 94    if not re.findall(r"[0-9]+.[0-9]+.[0-9]+", current_version):
 95        raise ValueError(
 96            f"{current_version} does not appear to match the required format of `x.x.x`."
 97        )
 98    bump_type = bump_type.lower().strip()
 99    if bump_type not in ["major", "minor", "patch"]:
100        raise ValueError(
101            f"`bump_type` {bump_type} is not one of `major`, `minor`, or `patch`."
102        )
103    major, minor, patch = [int(part) for part in current_version.split(".")]
104    if bump_type == "major":
105        major += 1
106        minor = 0
107        patch = 0
108    elif bump_type == "minor":
109        minor += 1
110        patch = 0
111    elif bump_type == "patch":
112        patch += 1
113    return f"{major}.{minor}.{patch}"

Bump current_version according to bump_type and return the new version.

:params:

current_version: A version string conforming to Semantic Versioning standards. i.e. {major}.{minor}.{patch}

bump_type can be one of major, minor, or patch.

Raises an exception if current_version is formatted incorrectly or if bump_type isn't one of the aforementioned types.

def get_dependencies( scandir: pathier.pathier.Pathier | pathlib.Path | str) -> list[tuple[str, str | None]]:
116def get_dependencies(scandir: Pathish) -> list[tuple[str, str | None]]:
117    """Scan `scandir` and return a list of tuples like `(package, version)`.
118
119    The version may be `None`."""
120    packages = packagelister.scan(scandir)
121    # Replace package names for packages known to have a different pip install name than import name
122    swaps = (root / "package_name_swaps.toml").loads()
123    for key in swaps:
124        packages = swap_keys(packages, (key, swaps[key]))
125    dependencies = []
126    for package in packages:
127        dependencies.append((package, packages[package].get("version")))
128    return dependencies

Scan scandir and return a list of tuples like (package, version).

The version may be None.

def format_dependency(dependency: tuple[str, str | None], include_version: bool) -> str:
131def format_dependency(dependency: tuple[str, str | None], include_version: bool) -> str:
132    """Format a dependency into a string.
133
134    If `include_version` and `dependency[1] is not None`, the return format will be `{package}~={version}`.
135
136    Otherwise, just `{package}`."""
137    if include_version and dependency[1]:
138        return f"{dependency[0]}~={dependency[1]}"
139    else:
140        return dependency[0]

Format a dependency into a string.

If include_version and dependency[1] is not None, the return format will be {package}~={version}.

Otherwise, just {package}.

def on_primary_branch() -> bool:
143def on_primary_branch() -> bool:
144    """Returns `False` if repo is not currently on `main` or `master` branch."""
145    git = Git(True)
146    if git.current_branch not in ["main", "master"]:
147        return False
148    return True

Returns False if repo is not currently on main or master branch.