hassle.hassle_utilities

  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    if git.current_branch not in ["main", "master"]:
193        return False
194    return True
def increment_version(pyproject_path: pathier.pathier.Pathier, increment_type: str):
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:
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:
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):
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):
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):
 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):
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):
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):
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:
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.