hassle.hassle_utilities API documentation

# hassle.hassle_utilities

View Source

      1import os
      2import subprocess
      3
      4import black
      5import packagelister
      6import requests
      7import vermin
      8from bs4 import BeautifulSoup
      9from gitbetter import Git
     10from pathier import Pathier
     11
     12from hassle import hassle_config
     13
     14root = Pathier(__file__).parent
     15
     16
     17def increment_version(pyproject_path: Pathier, increment_type: str):
     18    """Increment the project.version field in pyproject.toml.
     19
     20    :param package_path: Path to the package/project directory.
     21
     22    :param increment_type: One from 'major', 'minor', or 'patch'."""
     23    meta = pyproject_path.loads()
     24    major, minor, patch = [int(num) for num in meta["project"]["version"].split(".")]
     25    if increment_type == "major":
     26        major += 1
     27        minor = 0
     28        patch = 0
     29    elif increment_type == "minor":
     30        minor += 1
     31        patch = 0
     32    elif increment_type == "patch":
     33        patch += 1
     34    incremented_version = ".".join(str(num) for num in [major, minor, patch])
     35    meta["project"]["version"] = incremented_version
     36    pyproject_path.dumps(meta)
     37
     38
     39def get_minimum_py_version(src: str) -> str:
     40    """Scan src with vermin and return minimum
     41    python version."""
     42    config = vermin.Config()
     43    config.add_backport("typing")
     44    config.add_backport("typing_extensions")
     45    config.set_eval_annotations(True)
     46    result = vermin.visit(src, config).minimum_versions()[1]
     47    return f"{result[0]}.{result[1]}"
     48
     49
     50def get_project_code(project_path: Pathier) -> str:
     51    """Read and return all code from project_path
     52    as one string."""
     53    return "\n".join(file.read_text() for file in project_path.rglob("*.py"))
     54
     55
     56def update_minimum_python_version(pyproject_path: Pathier):
     57    """Use vermin to determine the minimum compatible
     58    Python version and update the corresponding field
     59    in pyproject.toml."""
     60    project_code = get_project_code(pyproject_path.parent / "src")
     61    meta = pyproject_path.loads()
     62    minimum_version = get_minimum_py_version(project_code)
     63    minimum_version = f">={minimum_version}"
     64    meta["project"]["requires-python"] = minimum_version
     65    pyproject_path.dumps(meta)
     66
     67
     68def generate_docs(package_path: Pathier):
     69    """Generate project documentation using pdoc."""
     70    try:
     71        (package_path / "docs").delete()
     72    except Exception as e:
     73        pass
     74    os.system(
     75        f"pdoc -o {package_path / 'docs'} {package_path / 'src' / package_path.stem}"
     76    )
     77
     78
     79def update_dependencies(
     80    pyproject_path: Pathier, overwrite: bool, include_versions: bool = False
     81):
     82    """Update dependencies list in pyproject.toml.
     83
     84    :param overwrite: If True, replace the dependencies in pyproject.toml
     85    with the results of packagelister.scan() .
     86    If False, packages returned by packagelister are appended to
     87    the current dependencies in pyproject.toml if they don't already
     88    exist in the field."""
     89    packages = packagelister.scan(pyproject_path.parent)
     90
     91    packages = [
     92        f"{package}~={packages[package]['version']}"
     93        if packages[package]["version"] and include_versions
     94        else f"{package}"
     95        for package in packages
     96        if package != pyproject_path.parent.stem
     97    ]
     98    packages = [
     99        package.replace("speech_recognition", "speechRecognition")
    100        for package in packages
    101    ]
    102    meta = pyproject_path.loads()
    103    if overwrite:
    104        meta["project"]["dependencies"] = packages
    105    else:
    106        for package in packages:
    107            if "~" in package:
    108                name = package.split("~")[0]
    109            elif "=" in package:
    110                name = package.split("=")[0]
    111            else:
    112                name = package
    113            if all(
    114                name not in dependency for dependency in meta["project"]["dependencies"]
    115            ):
    116                meta["project"]["dependencies"].append(package)
    117    pyproject_path.dumps(meta)
    118
    119
    120def update_changelog(pyproject_path: Pathier):
    121    """Update project changelog."""
    122    if hassle_config.config_exists():
    123        config = hassle_config.load_config()
    124    else:
    125        hassle_config.warn()
    126        print("Creating blank hassle_config.toml...")
    127        config = hassle_config.load_config()
    128    changelog_path = pyproject_path.parent / "CHANGELOG.md"
    129    raw_changelog = [
    130        line
    131        for line in subprocess.run(
    132            [
    133                "auto-changelog",
    134                "-p",
    135                pyproject_path.parent,
    136                "--tag-prefix",
    137                config["git"]["tag_prefix"],
    138                "--stdout",
    139            ],
    140            stdout=subprocess.PIPE,
    141            text=True,
    142        ).stdout.splitlines(True)
    143        if not line.startswith(
    144            (
    145                "Full set of changes:",
    146                f"* build {config['git']['tag_prefix']}",
    147                "* update changelog",
    148            )
    149        )
    150    ]
    151    if changelog_path.exists():
    152        previous_changelog = changelog_path.read_text().splitlines(True)[
    153            2:
    154        ]  # First two elements are "# Changelog\n" and "\n"
    155        for line in previous_changelog:
    156            # Release headers are prefixed with "## "
    157            if line.startswith("## "):
    158                new_changes = raw_changelog[: raw_changelog.index(line)]
    159                break
    160    else:
    161        new_changes = raw_changelog
    162        previous_changelog = []
    163    # if new_changes == "# Changelog\n\n" then there were no new changes
    164    if not "".join(new_changes) == "# Changelog\n\n":
    165        changelog_path.write_text("".join(new_changes + previous_changelog))
    166
    167
    168def tag_version(package_path: Pathier):
    169    """Add a git tag corresponding to the version number in pyproject.toml."""
    170    if hassle_config.config_exists():
    171        tag_prefix = hassle_config.load_config()["git"]["tag_prefix"]
    172    else:
    173        hassle_config.warn()
    174        tag_prefix = ""
    175    version = (package_path / "pyproject.toml").loads()["project"]["version"]
    176    os.chdir(package_path)
    177    git = Git()
    178    git.tag(f"{tag_prefix}{version}")
    179
    180
    181def format_files(path: Pathier):
    182    """Use `Black` to format file(s)."""
    183    try:
    184        black.main([str(path)])
    185    except SystemExit:
    186        ...
    187
    188
    189def on_primary_branch() -> bool:
    190    """Returns `False` if repo is not currently on `main` or `master` branch."""
    191    git = Git(True)
    192    if git.current_branch not in ["main", "master"]:
    193        return False
    194    return True
    195
    196
    197def latest_version_is_published(pyproject_path: Pathier) -> bool:
    198    """Return `True` if the version number in `pyproject.toml` and the project page on `pypi.org` agree."""
    199    data = pyproject_path.loads()
    200    name = data["project"]["name"]
    201    version = data["project"]["version"]
    202    pypi_url = f"https://pypi.org/project/{name}"
    203    response = requests.get(pypi_url)
    204    if response.status_code != 200:
    205        raise RuntimeError(f"{pypi_url} returned status code {response.status_code} :/")
    206    soup = BeautifulSoup(response.text, "html.parser")
    207    header = soup.find("h1", class_="package-header__name").text.strip()
    208    pypi_version = header[header.rfind(" ") + 1 :]
    209    return version == pypi_version

def increment_version(pyproject_path: pathier.pathier.Pathier, increment_type: str): View Source

    18def increment_version(pyproject_path: Pathier, increment_type: str):
    19    """Increment the project.version field in pyproject.toml.
    20
    21    :param package_path: Path to the package/project directory.
    22
    23    :param increment_type: One from 'major', 'minor', or 'patch'."""
    24    meta = pyproject_path.loads()
    25    major, minor, patch = [int(num) for num in meta["project"]["version"].split(".")]
    26    if increment_type == "major":
    27        major += 1
    28        minor = 0
    29        patch = 0
    30    elif increment_type == "minor":
    31        minor += 1
    32        patch = 0
    33    elif increment_type == "patch":
    34        patch += 1
    35    incremented_version = ".".join(str(num) for num in [major, minor, patch])
    36    meta["project"]["version"] = incremented_version
    37    pyproject_path.dumps(meta)

Increment the project.version field in pyproject.toml.

###### Parameters

- package_path: Path to the package/project directory.

- increment_type: One from 'major', 'minor', or 'patch'.

def get_minimum_py_version(src: str) -> str: View Source

    40def get_minimum_py_version(src: str) -> str:
    41    """Scan src with vermin and return minimum
    42    python version."""
    43    config = vermin.Config()
    44    config.add_backport("typing")
    45    config.add_backport("typing_extensions")
    46    config.set_eval_annotations(True)
    47    result = vermin.visit(src, config).minimum_versions()[1]
    48    return f"{result[0]}.{result[1]}"

Scan src with vermin and return minimum python version.

def get_project_code(project_path: pathier.pathier.Pathier) -> str: View Source

    51def get_project_code(project_path: Pathier) -> str:
    52    """Read and return all code from project_path
    53    as one string."""
    54    return "\n".join(file.read_text() for file in project_path.rglob("*.py"))

Read and return all code from project_path as one string.

def update_minimum_python_version(pyproject_path: pathier.pathier.Pathier): View Source

    57def update_minimum_python_version(pyproject_path: Pathier):
    58    """Use vermin to determine the minimum compatible
    59    Python version and update the corresponding field
    60    in pyproject.toml."""
    61    project_code = get_project_code(pyproject_path.parent / "src")
    62    meta = pyproject_path.loads()
    63    minimum_version = get_minimum_py_version(project_code)
    64    minimum_version = f">={minimum_version}"
    65    meta["project"]["requires-python"] = minimum_version
    66    pyproject_path.dumps(meta)

Use vermin to determine the minimum compatible Python version and update the corresponding field in pyproject.toml.

def generate_docs(package_path: pathier.pathier.Pathier): View Source

    69def generate_docs(package_path: Pathier):
    70    """Generate project documentation using pdoc."""
    71    try:
    72        (package_path / "docs").delete()
    73    except Exception as e:
    74        pass
    75    os.system(
    76        f"pdoc -o {package_path / 'docs'} {package_path / 'src' / package_path.stem}"
    77    )

Generate project documentation using pdoc.

def update_dependencies( pyproject_path: pathier.pathier.Pathier, overwrite: bool, include_versions: bool = False): View Source

     80def update_dependencies(
     81    pyproject_path: Pathier, overwrite: bool, include_versions: bool = False
     82):
     83    """Update dependencies list in pyproject.toml.
     84
     85    :param overwrite: If True, replace the dependencies in pyproject.toml
     86    with the results of packagelister.scan() .
     87    If False, packages returned by packagelister are appended to
     88    the current dependencies in pyproject.toml if they don't already
     89    exist in the field."""
     90    packages = packagelister.scan(pyproject_path.parent)
     91
     92    packages = [
     93        f"{package}~={packages[package]['version']}"
     94        if packages[package]["version"] and include_versions
     95        else f"{package}"
     96        for package in packages
     97        if package != pyproject_path.parent.stem
     98    ]
     99    packages = [
    100        package.replace("speech_recognition", "speechRecognition")
    101        for package in packages
    102    ]
    103    meta = pyproject_path.loads()
    104    if overwrite:
    105        meta["project"]["dependencies"] = packages
    106    else:
    107        for package in packages:
    108            if "~" in package:
    109                name = package.split("~")[0]
    110            elif "=" in package:
    111                name = package.split("=")[0]
    112            else:
    113                name = package
    114            if all(
    115                name not in dependency for dependency in meta["project"]["dependencies"]
    116            ):
    117                meta["project"]["dependencies"].append(package)
    118    pyproject_path.dumps(meta)

Update dependencies list in pyproject.toml.

###### Parameters

- overwrite: If True, replace the dependencies in pyproject.toml with the results of packagelister.scan() . If False, packages returned by packagelister are appended to the current dependencies in pyproject.toml if they don't already exist in the field.

def update_changelog(pyproject_path: pathier.pathier.Pathier): View Source

    121def update_changelog(pyproject_path: Pathier):
    122    """Update project changelog."""
    123    if hassle_config.config_exists():
    124        config = hassle_config.load_config()
    125    else:
    126        hassle_config.warn()
    127        print("Creating blank hassle_config.toml...")
    128        config = hassle_config.load_config()
    129    changelog_path = pyproject_path.parent / "CHANGELOG.md"
    130    raw_changelog = [
    131        line
    132        for line in subprocess.run(
    133            [
    134                "auto-changelog",
    135                "-p",
    136                pyproject_path.parent,
    137                "--tag-prefix",
    138                config["git"]["tag_prefix"],
    139                "--stdout",
    140            ],
    141            stdout=subprocess.PIPE,
    142            text=True,
    143        ).stdout.splitlines(True)
    144        if not line.startswith(
    145            (
    146                "Full set of changes:",
    147                f"* build {config['git']['tag_prefix']}",
    148                "* update changelog",
    149            )
    150        )
    151    ]
    152    if changelog_path.exists():
    153        previous_changelog = changelog_path.read_text().splitlines(True)[
    154            2:
    155        ]  # First two elements are "# Changelog\n" and "\n"
    156        for line in previous_changelog:
    157            # Release headers are prefixed with "## "
    158            if line.startswith("## "):
    159                new_changes = raw_changelog[: raw_changelog.index(line)]
    160                break
    161    else:
    162        new_changes = raw_changelog
    163        previous_changelog = []
    164    # if new_changes == "# Changelog\n\n" then there were no new changes
    165    if not "".join(new_changes) == "# Changelog\n\n":
    166        changelog_path.write_text("".join(new_changes + previous_changelog))

Update project changelog.

def tag_version(package_path: pathier.pathier.Pathier): View Source

    169def tag_version(package_path: Pathier):
    170    """Add a git tag corresponding to the version number in pyproject.toml."""
    171    if hassle_config.config_exists():
    172        tag_prefix = hassle_config.load_config()["git"]["tag_prefix"]
    173    else:
    174        hassle_config.warn()
    175        tag_prefix = ""
    176    version = (package_path / "pyproject.toml").loads()["project"]["version"]
    177    os.chdir(package_path)
    178    git = Git()
    179    git.tag(f"{tag_prefix}{version}")

Add a git tag corresponding to the version number in pyproject.toml.

def format_files(path: pathier.pathier.Pathier): View Source

    182def format_files(path: Pathier):
    183    """Use `Black` to format file(s)."""
    184    try:
    185        black.main([str(path)])
    186    except SystemExit:
    187        ...

Use `Black` to format file(s).

def on_primary_branch() -> bool: View Source

    190def on_primary_branch() -> bool:
    191    """Returns `False` if repo is not currently on `main` or `master` branch."""
    192    git = Git(True)
    193    if git.current_branch not in ["main", "master"]:
    194        return False
    195    return True

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

def latest_version_is_published(pyproject_path: pathier.pathier.Pathier) -> bool: View Source

    198def latest_version_is_published(pyproject_path: Pathier) -> bool:
    199    """Return `True` if the version number in `pyproject.toml` and the project page on `pypi.org` agree."""
    200    data = pyproject_path.loads()
    201    name = data["project"]["name"]
    202    version = data["project"]["version"]
    203    pypi_url = f"https://pypi.org/project/{name}"
    204    response = requests.get(pypi_url)
    205    if response.status_code != 200:
    206        raise RuntimeError(f"{pypi_url} returned status code {response.status_code} :/")
    207    soup = BeautifulSoup(response.text, "html.parser")
    208    header = soup.find("h1", class_="package-header__name").text.strip()
    209    pypi_version = header[header.rfind(" ") + 1 :]
    210    return version == pypi_version

Return `True` if the version number in `pyproject.toml` and the project page on `pypi.org` agree.