hassle.utilities API documentation

# hassle.utilities

View Source

      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]): View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

     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]]: View Source

    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: View Source

    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: View Source

    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.