hassle.utilities

  1import re
  2import subprocess
  3
  4import coverage
  5import pytest
  6import requests
  7from bs4 import BeautifulSoup
  8from gitbetter import Git
  9from pathier import Pathier
 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    results = subprocess.run(["coverage", "run", "-m", "pytest", "-s"])
 39    subprocess.run(["coverage", "report", f"--include={Pathier.cwd()}/*"])
 40    subprocess.run(["coverage", "html", f"--include={Pathier.cwd()}/*"])
 41    return results.returncode in [0, 5]
 42
 43
 44def check_pypi(package_name: str) -> bool:
 45    """Check if a package with package_name already exists on `pypi.org`.
 46    Returns `True` if package name exists.
 47    Only checks the first page of results."""
 48    url = f"https://pypi.org/search/?q={package_name.lower()}"
 49    response = requests.get(url)
 50    if response.status_code != 200:
 51        raise RuntimeError(
 52            f"Error: pypi.org returned status code: {response.status_code}"
 53        )
 54    soup = BeautifulSoup(response.text, "html.parser")
 55    pypi_packages = [
 56        span.text.lower()
 57        for span in soup.find_all("span", class_="package-snippet__name")
 58    ]
 59    return package_name in pypi_packages
 60
 61
 62def get_answer(question: str) -> bool | None:
 63    """Repeatedly ask the user a yes/no question until a 'y' or a 'n' is received."""
 64    ans = ""
 65    question = question.strip()
 66    if "?" not in question:
 67        question += "?"
 68    question += " (y/n): "
 69    while ans not in ["y", "yes", "no", "n"]:
 70        ans = input(question).strip().lower()
 71        if ans in ["y", "yes"]:
 72            return True
 73        elif ans in ["n", "no"]:
 74            return False
 75        else:
 76            print("Invalid answer.")
 77
 78
 79def bump_version(current_version: str, bump_type: str) -> str:
 80    """Bump `current_version` according to `bump_type` and return the new version.
 81
 82    #### :params:
 83
 84    `current_version`: A version string conforming to Semantic Versioning standards.
 85    i.e. `{major}.{minor}.{patch}`
 86
 87    `bump_type` can be one of `major`, `minor`, or `patch`.
 88
 89    Raises an exception if `current_version` is formatted incorrectly or if `bump_type` isn't one of the aforementioned types.
 90    """
 91    if not re.findall(r"[0-9]+.[0-9]+.[0-9]+", current_version):
 92        raise ValueError(
 93            f"{current_version} does not appear to match the required format of `x.x.x`."
 94        )
 95    bump_type = bump_type.lower().strip()
 96    if bump_type not in ["major", "minor", "patch"]:
 97        raise ValueError(
 98            f"`bump_type` {bump_type} is not one of `major`, `minor`, or `patch`."
 99        )
100    major, minor, patch = [int(part) for part in current_version.split(".")]
101    if bump_type == "major":
102        major += 1
103        minor = 0
104        patch = 0
105    elif bump_type == "minor":
106        minor += 1
107        patch = 0
108    elif bump_type == "patch":
109        patch += 1
110    return f"{major}.{minor}.{patch}"
111
112
113def on_primary_branch() -> bool:
114    """Returns `False` if repo is not currently on `main` or `master` branch."""
115    git = Git(True)
116    if git.current_branch not in ["main", "master"]:
117        return False
118    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    results = subprocess.run(["coverage", "run", "-m", "pytest", "-s"])
40    subprocess.run(["coverage", "report", f"--include={Pathier.cwd()}/*"])
41    subprocess.run(["coverage", "html", f"--include={Pathier.cwd()}/*"])
42    return results.returncode 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:
45def check_pypi(package_name: str) -> bool:
46    """Check if a package with package_name already exists on `pypi.org`.
47    Returns `True` if package name exists.
48    Only checks the first page of results."""
49    url = f"https://pypi.org/search/?q={package_name.lower()}"
50    response = requests.get(url)
51    if response.status_code != 200:
52        raise RuntimeError(
53            f"Error: pypi.org returned status code: {response.status_code}"
54        )
55    soup = BeautifulSoup(response.text, "html.parser")
56    pypi_packages = [
57        span.text.lower()
58        for span in soup.find_all("span", class_="package-snippet__name")
59    ]
60    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:
63def get_answer(question: str) -> bool | None:
64    """Repeatedly ask the user a yes/no question until a 'y' or a 'n' is received."""
65    ans = ""
66    question = question.strip()
67    if "?" not in question:
68        question += "?"
69    question += " (y/n): "
70    while ans not in ["y", "yes", "no", "n"]:
71        ans = input(question).strip().lower()
72        if ans in ["y", "yes"]:
73            return True
74        elif ans in ["n", "no"]:
75            return False
76        else:
77            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:
 80def bump_version(current_version: str, bump_type: str) -> str:
 81    """Bump `current_version` according to `bump_type` and return the new version.
 82
 83    #### :params:
 84
 85    `current_version`: A version string conforming to Semantic Versioning standards.
 86    i.e. `{major}.{minor}.{patch}`
 87
 88    `bump_type` can be one of `major`, `minor`, or `patch`.
 89
 90    Raises an exception if `current_version` is formatted incorrectly or if `bump_type` isn't one of the aforementioned types.
 91    """
 92    if not re.findall(r"[0-9]+.[0-9]+.[0-9]+", current_version):
 93        raise ValueError(
 94            f"{current_version} does not appear to match the required format of `x.x.x`."
 95        )
 96    bump_type = bump_type.lower().strip()
 97    if bump_type not in ["major", "minor", "patch"]:
 98        raise ValueError(
 99            f"`bump_type` {bump_type} is not one of `major`, `minor`, or `patch`."
100        )
101    major, minor, patch = [int(part) for part in current_version.split(".")]
102    if bump_type == "major":
103        major += 1
104        minor = 0
105        patch = 0
106    elif bump_type == "minor":
107        minor += 1
108        patch = 0
109    elif bump_type == "patch":
110        patch += 1
111    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 on_primary_branch() -> bool:
114def on_primary_branch() -> bool:
115    """Returns `False` if repo is not currently on `main` or `master` branch."""
116    git = Git(True)
117    if git.current_branch not in ["main", "master"]:
118        return False
119    return True

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