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

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

    18def update_init_version(pyproject_path: Pathier):
    19    project = pyproject_path.loads()["project"]
    20    version = project["version"]
    21    name = project["name"]
    22    init_path: Pathier = pyproject_path.parent / "src" / name / "__init__.py"
    23    content = init_path.read_text()
    24    if "__version__" in content:
    25        lines = content.splitlines()
    26        for i, line in enumerate(lines):
    27            if "__version__" in line:
    28                lines[i] = f'__version__ = "{version}"'
    29                break
    30        content = "\n".join(lines)
    31    else:
    32        content = content.strip("\n") + f'__version__ = "{version}"\n'
    33    init_path.write_text(content)

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

    36def increment_version(pyproject_path: Pathier, increment_type: str):
    37    """Increment the project.version field in pyproject.toml and `__version__` in `__init__.py`.
    38
    39    :param package_path: Path to the package/project directory.
    40
    41    :param increment_type: One from 'major', 'minor', or 'patch'."""
    42    meta = pyproject_path.loads()
    43    major, minor, patch = [int(num) for num in meta["project"]["version"].split(".")]
    44    if increment_type == "major":
    45        major += 1
    46        minor = 0
    47        patch = 0
    48    elif increment_type == "minor":
    49        minor += 1
    50        patch = 0
    51    elif increment_type == "patch":
    52        patch += 1
    53    incremented_version = ".".join(str(num) for num in [major, minor, patch])
    54    meta["project"]["version"] = incremented_version
    55    pyproject_path.dumps(meta)
    56    update_init_version(pyproject_path)

Increment the project.version field in pyproject.toml and `__version__` in `__init__.py`.

###### 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

    59def get_minimum_py_version(src: str) -> str:
    60    """Scan src with vermin and return minimum
    61    python version."""
    62    config = vermin.Config()
    63    config.add_backport("typing")
    64    config.add_backport("typing_extensions")
    65    config.set_eval_annotations(True)
    66    result = vermin.visit(src, config).minimum_versions()[1]
    67    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

    70def get_project_code(project_path: Pathier) -> str:
    71    """Read and return all code from project_path
    72    as one string."""
    73    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

    76def update_minimum_python_version(pyproject_path: Pathier):
    77    """Use vermin to determine the minimum compatible
    78    Python version and update the corresponding field
    79    in pyproject.toml."""
    80    project_code = get_project_code(pyproject_path.parent / "src")
    81    meta = pyproject_path.loads()
    82    minimum_version = get_minimum_py_version(project_code)
    83    minimum_version = f">={minimum_version}"
    84    meta["project"]["requires-python"] = minimum_version
    85    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

    88def generate_docs(package_path: Pathier):
    89    """Generate project documentation using pdoc."""
    90    try:
    91        (package_path / "docs").delete()
    92    except Exception as e:
    93        pass
    94    os.system(
    95        f"pdoc -o {package_path / 'docs'} {package_path / 'src' / package_path.stem}"
    96    )

Generate project documentation using pdoc.

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

     99def update_dependencies(
    100    pyproject_path: Pathier, overwrite: bool, include_versions: bool = False
    101):
    102    """Update dependencies list in pyproject.toml.
    103
    104    :param overwrite: If True, replace the dependencies in pyproject.toml
    105    with the results of packagelister.scan() .
    106    If False, packages returned by packagelister are appended to
    107    the current dependencies in pyproject.toml if they don't already
    108    exist in the field."""
    109    packages = packagelister.scan(pyproject_path.parent)
    110
    111    packages = [
    112        f"{package}~={packages[package]['version']}"
    113        if packages[package]["version"] and include_versions
    114        else f"{package}"
    115        for package in packages
    116        if package != pyproject_path.parent.stem
    117    ]
    118    packages = [
    119        package.replace("speech_recognition", "speechRecognition")
    120        for package in packages
    121    ]
    122    meta = pyproject_path.loads()
    123    if overwrite:
    124        meta["project"]["dependencies"] = packages
    125    else:
    126        for package in packages:
    127            if "~" in package:
    128                name = package.split("~")[0]
    129            elif "=" in package:
    130                name = package.split("=")[0]
    131            else:
    132                name = package
    133            if all(
    134                name not in dependency for dependency in meta["project"]["dependencies"]
    135            ):
    136                meta["project"]["dependencies"].append(package)
    137    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

    140def update_changelog(pyproject_path: Pathier):
    141    """Update project changelog."""
    142    if hassle_config.config_exists():
    143        config = hassle_config.load_config()
    144    else:
    145        hassle_config.warn()
    146        print("Creating blank hassle_config.toml...")
    147        config = hassle_config.load_config()
    148    changelog_path = pyproject_path.parent / "CHANGELOG.md"
    149    raw_changelog = [
    150        line
    151        for line in subprocess.run(
    152            [
    153                "auto-changelog",
    154                "-p",
    155                pyproject_path.parent,
    156                "--tag-prefix",
    157                config["git"]["tag_prefix"],
    158                "--stdout",
    159            ],
    160            stdout=subprocess.PIPE,
    161            text=True,
    162        ).stdout.splitlines(True)
    163        if not line.startswith(
    164            (
    165                "Full set of changes:",
    166                f"* build {config['git']['tag_prefix']}",
    167                "* update changelog",
    168            )
    169        )
    170    ]
    171    if changelog_path.exists():
    172        previous_changelog = changelog_path.read_text().splitlines(True)[
    173            2:
    174        ]  # First two elements are "# Changelog\n" and "\n"
    175        for line in previous_changelog:
    176            # Release headers are prefixed with "## "
    177            if line.startswith("## "):
    178                new_changes = raw_changelog[: raw_changelog.index(line)]
    179                break
    180    else:
    181        new_changes = raw_changelog
    182        previous_changelog = []
    183    # if new_changes == "# Changelog\n\n" then there were no new changes
    184    if not "".join(new_changes) == "# Changelog\n\n":
    185        changelog_path.write_text("".join(new_changes + previous_changelog))

Update project changelog.

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

    188def tag_version(package_path: Pathier):
    189    """Add a git tag corresponding to the version number in pyproject.toml."""
    190    if hassle_config.config_exists():
    191        tag_prefix = hassle_config.load_config()["git"]["tag_prefix"]
    192    else:
    193        hassle_config.warn()
    194        tag_prefix = ""
    195    version = (package_path / "pyproject.toml").loads()["project"]["version"]
    196    os.chdir(package_path)
    197    git = Git()
    198    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

    201def format_files(path: Pathier):
    202    """Use `Black` to format file(s)."""
    203    try:
    204        black.main([str(path)])
    205    except SystemExit:
    206        ...

Use `Black` to format file(s).

def on_primary_branch() -> bool: View Source

    209def on_primary_branch() -> bool:
    210    """Returns `False` if repo is not currently on `main` or `master` branch."""
    211    git = Git(True)
    212    if git.current_branch not in ["main", "master"]:
    213        return False
    214    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

    217def latest_version_is_published(pyproject_path: Pathier) -> bool:
    218    """Return `True` if the version number in `pyproject.toml` and the project page on `pypi.org` agree."""
    219    data = pyproject_path.loads()
    220    name = data["project"]["name"]
    221    version = data["project"]["version"]
    222    pypi_url = f"https://pypi.org/project/{name}"
    223    response = requests.get(pypi_url)
    224    if response.status_code != 200:
    225        raise RuntimeError(f"{pypi_url} returned status code {response.status_code} :/")
    226    soup = BeautifulSoup(response.text, "html.parser")
    227    header = soup.find("h1", class_="package-header__name").text.strip()
    228    pypi_version = header[header.rfind(" ") + 1 :]
    229    return version == pypi_version

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