hassle.hassle_utilities
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
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)
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'.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.