hassle.utilities
1import re 2 3import coverage 4import packagelister 5import pytest 6import requests 7from bs4 import BeautifulSoup 8from gitbetter import Git 9from pathier import Pathier, Pathish 10 11root = Pathier(__file__).parent 12 13 14def swap_keys(data: dict, keys: tuple[str, str]): 15 """Convert between keys in `data`. 16 The order of `keys` doesn't matter. 17 >>> data = {"one two": 1} 18 >>> data = swap_keys(data, ("one two", "one-two")) 19 >>> print(data) 20 >>> {"one-two": 1} 21 >>> data = swap_keys(data, ("one two", "one-two")) 22 >>> print(data) 23 >>> {"one two": 1} 24 """ 25 key1, key2 = keys 26 data_keys = data.keys() 27 if key1 in data_keys: 28 data[key2] = data.pop(key1) 29 elif key2 in data_keys: 30 data[key1] = data.pop(key2) 31 return data 32 33 34def run_tests() -> bool: 35 """Invoke `coverage` and `pytest -s`. 36 37 Returns `True` if all tests passed or if no tests were found.""" 38 cover = coverage.Coverage() 39 cover.start() 40 results = pytest.main(["-s"]) 41 cover.stop() 42 cover.report() 43 return results in [0, 5] 44 45 46def check_pypi(package_name: str) -> bool: 47 """Check if a package with package_name already exists on `pypi.org`. 48 Returns `True` if package name exists. 49 Only checks the first page of results.""" 50 url = f"https://pypi.org/search/?q={package_name.lower()}" 51 response = requests.get(url) 52 if response.status_code != 200: 53 raise RuntimeError( 54 f"Error: pypi.org returned status code: {response.status_code}" 55 ) 56 soup = BeautifulSoup(response.text, "html.parser") 57 pypi_packages = [ 58 span.text.lower() 59 for span in soup.find_all("span", class_="package-snippet__name") 60 ] 61 return package_name in pypi_packages 62 63 64def get_answer(question: str) -> bool | None: 65 """Repeatedly ask the user a yes/no question until a 'y' or a 'n' is received.""" 66 ans = "" 67 question = question.strip() 68 if "?" not in question: 69 question += "?" 70 question += " (y/n): " 71 while ans not in ["y", "yes", "no", "n"]: 72 ans = input(question).strip().lower() 73 if ans in ["y", "yes"]: 74 return True 75 elif ans in ["n", "no"]: 76 return False 77 else: 78 print("Invalid answer.") 79 80 81def bump_version(current_version: str, bump_type: str) -> str: 82 """Bump `current_version` according to `bump_type` and return the new version. 83 84 #### :params: 85 86 `current_version`: A version string conforming to Semantic Versioning standards. 87 i.e. `{major}.{minor}.{patch}` 88 89 `bump_type` can be one of `major`, `minor`, or `patch`. 90 91 Raises an exception if `current_version` is formatted incorrectly or if `bump_type` isn't one of the aforementioned types. 92 """ 93 if not re.findall(r"[0-9]+.[0-9]+.[0-9]+", current_version): 94 raise ValueError( 95 f"{current_version} does not appear to match the required format of `x.x.x`." 96 ) 97 bump_type = bump_type.lower().strip() 98 if bump_type not in ["major", "minor", "patch"]: 99 raise ValueError( 100 f"`bump_type` {bump_type} is not one of `major`, `minor`, or `patch`." 101 ) 102 major, minor, patch = [int(part) for part in current_version.split(".")] 103 if bump_type == "major": 104 major += 1 105 minor = 0 106 patch = 0 107 elif bump_type == "minor": 108 minor += 1 109 patch = 0 110 elif bump_type == "patch": 111 patch += 1 112 return f"{major}.{minor}.{patch}" 113 114 115def get_dependencies(scandir: Pathish) -> list[tuple[str, str | None]]: 116 """Scan `scandir` and return a list of tuples like `(package, version)`. 117 118 The version may be `None`.""" 119 packages = packagelister.scan(scandir) 120 # Replace package names for packages known to have a different pip install name than import name 121 swaps = (root / "package_name_swaps.toml").loads() 122 for key in swaps: 123 packages = swap_keys(packages, (key, swaps[key])) 124 dependencies = [] 125 for package in packages: 126 dependencies.append((package, packages[package].get("version"))) 127 return dependencies 128 129 130def format_dependency(dependency: tuple[str, str | None], include_version: bool) -> str: 131 """Format a dependency into a string. 132 133 If `include_version` and `dependency[1] is not None`, the return format will be `{package}~={version}`. 134 135 Otherwise, just `{package}`.""" 136 if include_version and dependency[1]: 137 return f"{dependency[0]}~={dependency[1]}" 138 else: 139 return dependency[0] 140 141 142def on_primary_branch() -> bool: 143 """Returns `False` if repo is not currently on `main` or `master` branch.""" 144 git = Git(True) 145 if git.current_branch not in ["main", "master"]: 146 return False 147 return True
15def swap_keys(data: dict, keys: tuple[str, str]): 16 """Convert between keys in `data`. 17 The order of `keys` doesn't matter. 18 >>> data = {"one two": 1} 19 >>> data = swap_keys(data, ("one two", "one-two")) 20 >>> print(data) 21 >>> {"one-two": 1} 22 >>> data = swap_keys(data, ("one two", "one-two")) 23 >>> print(data) 24 >>> {"one two": 1} 25 """ 26 key1, key2 = keys 27 data_keys = data.keys() 28 if key1 in data_keys: 29 data[key2] = data.pop(key1) 30 elif key2 in data_keys: 31 data[key1] = data.pop(key2) 32 return data
Convert between keys in data
.
The order of keys
doesn't matter.
>>> data = {"one two": 1}
>>> data = swap_keys(data, ("one two", "one-two"))
>>> print(data)
>>> {"one-two": 1}
>>> data = swap_keys(data, ("one two", "one-two"))
>>> print(data)
>>> {"one two": 1}
35def run_tests() -> bool: 36 """Invoke `coverage` and `pytest -s`. 37 38 Returns `True` if all tests passed or if no tests were found.""" 39 cover = coverage.Coverage() 40 cover.start() 41 results = pytest.main(["-s"]) 42 cover.stop() 43 cover.report() 44 return results in [0, 5]
Invoke coverage
and pytest -s
.
Returns True
if all tests passed or if no tests were found.
47def check_pypi(package_name: str) -> bool: 48 """Check if a package with package_name already exists on `pypi.org`. 49 Returns `True` if package name exists. 50 Only checks the first page of results.""" 51 url = f"https://pypi.org/search/?q={package_name.lower()}" 52 response = requests.get(url) 53 if response.status_code != 200: 54 raise RuntimeError( 55 f"Error: pypi.org returned status code: {response.status_code}" 56 ) 57 soup = BeautifulSoup(response.text, "html.parser") 58 pypi_packages = [ 59 span.text.lower() 60 for span in soup.find_all("span", class_="package-snippet__name") 61 ] 62 return package_name in pypi_packages
Check if a package with package_name already exists on pypi.org
.
Returns True
if package name exists.
Only checks the first page of results.
65def get_answer(question: str) -> bool | None: 66 """Repeatedly ask the user a yes/no question until a 'y' or a 'n' is received.""" 67 ans = "" 68 question = question.strip() 69 if "?" not in question: 70 question += "?" 71 question += " (y/n): " 72 while ans not in ["y", "yes", "no", "n"]: 73 ans = input(question).strip().lower() 74 if ans in ["y", "yes"]: 75 return True 76 elif ans in ["n", "no"]: 77 return False 78 else: 79 print("Invalid answer.")
Repeatedly ask the user a yes/no question until a 'y' or a 'n' is received.
82def bump_version(current_version: str, bump_type: str) -> str: 83 """Bump `current_version` according to `bump_type` and return the new version. 84 85 #### :params: 86 87 `current_version`: A version string conforming to Semantic Versioning standards. 88 i.e. `{major}.{minor}.{patch}` 89 90 `bump_type` can be one of `major`, `minor`, or `patch`. 91 92 Raises an exception if `current_version` is formatted incorrectly or if `bump_type` isn't one of the aforementioned types. 93 """ 94 if not re.findall(r"[0-9]+.[0-9]+.[0-9]+", current_version): 95 raise ValueError( 96 f"{current_version} does not appear to match the required format of `x.x.x`." 97 ) 98 bump_type = bump_type.lower().strip() 99 if bump_type not in ["major", "minor", "patch"]: 100 raise ValueError( 101 f"`bump_type` {bump_type} is not one of `major`, `minor`, or `patch`." 102 ) 103 major, minor, patch = [int(part) for part in current_version.split(".")] 104 if bump_type == "major": 105 major += 1 106 minor = 0 107 patch = 0 108 elif bump_type == "minor": 109 minor += 1 110 patch = 0 111 elif bump_type == "patch": 112 patch += 1 113 return f"{major}.{minor}.{patch}"
Bump current_version
according to bump_type
and return the new version.
:params:
current_version
: A version string conforming to Semantic Versioning standards.
i.e. {major}.{minor}.{patch}
bump_type
can be one of major
, minor
, or patch
.
Raises an exception if current_version
is formatted incorrectly or if bump_type
isn't one of the aforementioned types.
116def get_dependencies(scandir: Pathish) -> list[tuple[str, str | None]]: 117 """Scan `scandir` and return a list of tuples like `(package, version)`. 118 119 The version may be `None`.""" 120 packages = packagelister.scan(scandir) 121 # Replace package names for packages known to have a different pip install name than import name 122 swaps = (root / "package_name_swaps.toml").loads() 123 for key in swaps: 124 packages = swap_keys(packages, (key, swaps[key])) 125 dependencies = [] 126 for package in packages: 127 dependencies.append((package, packages[package].get("version"))) 128 return dependencies
Scan scandir
and return a list of tuples like (package, version)
.
The version may be None
.
131def format_dependency(dependency: tuple[str, str | None], include_version: bool) -> str: 132 """Format a dependency into a string. 133 134 If `include_version` and `dependency[1] is not None`, the return format will be `{package}~={version}`. 135 136 Otherwise, just `{package}`.""" 137 if include_version and dependency[1]: 138 return f"{dependency[0]}~={dependency[1]}" 139 else: 140 return dependency[0]
Format a dependency into a string.
If include_version
and dependency[1] is not None
, the return format will be {package}~={version}
.
Otherwise, just {package}
.
143def on_primary_branch() -> bool: 144 """Returns `False` if repo is not currently on `main` or `master` branch.""" 145 git = Git(True) 146 if git.current_branch not in ["main", "master"]: 147 return False 148 return True
Returns False
if repo is not currently on main
or master
branch.