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