hassle.hassle_utilities API documentation

# hassle.hassle_utilities

View Source

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

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

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

    38def get_minimum_py_version(src: str) -> str:
    39    """Scan src with vermin and return minimum
    40    python version."""
    41    config = vermin.Config()
    42    config.add_backport("typing")
    43    config.add_backport("typing_extensions")
    44    config.set_eval_annotations(True)
    45    result = vermin.visit(src, config).minimum_versions()[1]
    46    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

    49def get_project_code(project_path: Pathier) -> str:
    50    """Read and return all code from project_path
    51    as one string."""
    52    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

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

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

Generate project documentation using pdoc.

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

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

    119def update_changelog(pyproject_path: Pathier):
    120    """Update project changelog."""
    121    meta = pyproject_path.loads()
    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))

Update project changelog.

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

    168def tag_version(package_path: Pathier):
    169    """Add a git tag corresponding
    170    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    branches = git.execute("branch").splitlines()
    194    for branch in branches:
    195        if branch.startswith("*"):
    196            current_branch = branch[2:]
    197            break
    198    if current_branch.lower() not in ["main", "master"]:
    199        return False
    200    return True

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