Coverage for src\hassle\utilities.py: 55%

65 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-02-15 19:47 -0600

1import re 

2 

3import coverage 

4import pytest 

5import requests 

6from bs4 import BeautifulSoup 

7from gitbetter import Git 

8from pathier import Pathier 

9import subprocess 

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 results = subprocess.run(["coverage", "run", "-m", "pytest", "-s"]) 

39 subprocess.run(["coverage", "report", f"--include={Pathier.cwd()}/*"]) 

40 subprocess.run(["coverage", "html", f"--include={Pathier.cwd()}/*"]) 

41 return results.returncode in [0, 5] 

42 

43 

44def check_pypi(package_name: str) -> bool: 

45 """Check if a package with package_name already exists on `pypi.org`. 

46 Returns `True` if package name exists. 

47 Only checks the first page of results.""" 

48 url = f"https://pypi.org/search/?q={package_name.lower()}" 

49 response = requests.get(url) 

50 if response.status_code != 200: 

51 raise RuntimeError( 

52 f"Error: pypi.org returned status code: {response.status_code}" 

53 ) 

54 soup = BeautifulSoup(response.text, "html.parser") 

55 pypi_packages = [ 

56 span.text.lower() 

57 for span in soup.find_all("span", class_="package-snippet__name") 

58 ] 

59 return package_name in pypi_packages 

60 

61 

62def get_answer(question: str) -> bool | None: 

63 """Repeatedly ask the user a yes/no question until a 'y' or a 'n' is received.""" 

64 ans = "" 

65 question = question.strip() 

66 if "?" not in question: 

67 question += "?" 

68 question += " (y/n): " 

69 while ans not in ["y", "yes", "no", "n"]: 

70 ans = input(question).strip().lower() 

71 if ans in ["y", "yes"]: 

72 return True 

73 elif ans in ["n", "no"]: 

74 return False 

75 else: 

76 print("Invalid answer.") 

77 

78 

79def bump_version(current_version: str, bump_type: str) -> str: 

80 """Bump `current_version` according to `bump_type` and return the new version. 

81 

82 #### :params: 

83 

84 `current_version`: A version string conforming to Semantic Versioning standards. 

85 i.e. `{major}.{minor}.{patch}` 

86 

87 `bump_type` can be one of `major`, `minor`, or `patch`. 

88 

89 Raises an exception if `current_version` is formatted incorrectly or if `bump_type` isn't one of the aforementioned types. 

90 """ 

91 if not re.findall(r"[0-9]+.[0-9]+.[0-9]+", current_version): 

92 raise ValueError( 

93 f"{current_version} does not appear to match the required format of `x.x.x`." 

94 ) 

95 bump_type = bump_type.lower().strip() 

96 if bump_type not in ["major", "minor", "patch"]: 

97 raise ValueError( 

98 f"`bump_type` {bump_type} is not one of `major`, `minor`, or `patch`." 

99 ) 

100 major, minor, patch = [int(part) for part in current_version.split(".")] 

101 if bump_type == "major": 

102 major += 1 

103 minor = 0 

104 patch = 0 

105 elif bump_type == "minor": 

106 minor += 1 

107 patch = 0 

108 elif bump_type == "patch": 

109 patch += 1 

110 return f"{major}.{minor}.{patch}" 

111 

112 

113def on_primary_branch() -> bool: 

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

115 git = Git(True) 

116 if git.current_branch not in ["main", "master"]: 

117 return False 

118 return True