hassle.hassle
1import argparse 2import os 3import sys 4 5import isort 6from gitbetter import Git 7from pathier import Pathier 8 9from hassle import hassle_utilities 10from hassle.generate_tests import generate_test_files 11from hassle.run_tests import run_tests 12 13root = Pathier(__file__).parent 14 15 16def get_args() -> argparse.Namespace: 17 parser = argparse.ArgumentParser() 18 19 parser.add_argument( 20 "package", 21 type=str, 22 default=".", 23 nargs="?", 24 help=""" The name of the package or project to use, 25 assuming it's a subfolder of your current working directory. 26 Can also be a full path to the package. If nothing is given, 27 the current working directory will be used.""", 28 ) 29 30 parser.add_argument( 31 "-b", "--build", action="store_true", help=""" Build the package. """ 32 ) 33 34 parser.add_argument( 35 "-t", 36 "--tag_version", 37 action="store_true", 38 help=""" Add a git tag corresponding to the version in pyproject.toml. """, 39 ) 40 41 parser.add_argument( 42 "-i", 43 "--install", 44 action="store_true", 45 help=""" Install the package from source. """, 46 ) 47 48 parser.add_argument( 49 "-iv", 50 "--increment_version", 51 type=str, 52 default=None, 53 choices=["major", "minor", "patch"], 54 help=""" Increment version in pyproject.toml. 55 Can be one of "major", "minor", or "patch". """, 56 ) 57 58 parser.add_argument( 59 "-p", 60 "--publish", 61 action="store_true", 62 help=""" Publish package to PyPi. 63 Note: You must have configured twine 64 and registered a PyPi account/generated an API 65 key to use this option.""", 66 ) 67 68 parser.add_argument( 69 "-rt", 70 "--run_tests", 71 action="store_true", 72 help=""" Run tests for the package. """, 73 ) 74 75 parser.add_argument( 76 "-gt", 77 "--generate_tests", 78 action="store_true", 79 help=""" Generate tests for the package. """, 80 ) 81 82 parser.add_argument( 83 "-uc", 84 "--update_changelog", 85 action="store_true", 86 help=""" Update changelog file. """, 87 ) 88 89 parser.add_argument( 90 "-od", 91 "--overwrite_dependencies", 92 action="store_true", 93 help=""" When building a package, packagelister will be used 94 to update the dependencies list in pyproject.toml. 95 The default behavior is to append any new dependencies to 96 the current list so as not to erase any manually added dependencies 97 that packagelister may not detect. If you don't have any manually 98 added dependencies and want to remove any dependencies that your 99 project no longer uses, pass this flag.""", 100 ) 101 102 parser.add_argument( 103 "-ca", 104 "--commit_all", 105 type=str, 106 default=None, 107 help=""" Git stage and commit all tracked files with this supplied commit message. 108 If 'build' is passed, all commits will have message: 'chore: build v{current_version}""", 109 ) 110 111 parser.add_argument( 112 "-s", 113 "--sync", 114 action="store_true", 115 help=""" Pull from github, then push current commit to repo. """, 116 ) 117 118 parser.add_argument( 119 "-dv", 120 "--dependency_versions", 121 action="store_true", 122 help=""" Include version specifiers for dependencies in 123 pyproject.toml.""", 124 ) 125 126 parser.add_argument( 127 "-up", 128 "--update", 129 type=str, 130 default=None, 131 choices=["major", "minor", "patch"], 132 help=""" Expects one argument: "major", "minor", or "patch". 133 Passing "-up minor" is equivalent to passing "--build --tag_version --increment_version minor --update_changelog --commit_all build --sync". 134 To publish the updated package, the -p/--publish switch needs to be added to the cli input. 135 To install the updated package, the -i/--install switch also needs to be added.""", 136 ) 137 138 parser.add_argument( 139 "-st", 140 "--skip_tests", 141 action="store_true", 142 help=""" Don't run tests when using the -b/--build command. """, 143 ) 144 145 parser.add_argument( 146 "-ip", 147 "--is_published", 148 action="store_true", 149 help=""" Check that the version number in `pyproject.toml` and `pypi.org/project/{project_name}` agree. """, 150 ) 151 152 args = parser.parse_args() 153 154 args.package = Pathier(args.package).resolve() 155 156 if args.update: 157 args.build = True 158 args.tag_version = True 159 args.increment_version = args.update 160 args.update_changelog = True 161 args.commit_all = "build" 162 args.sync = True 163 164 if args.increment_version and args.increment_version not in [ 165 "major", 166 "minor", 167 "patch", 168 ]: 169 raise ValueError( 170 f"Invalid option for -iv/--increment_version: {args.increment_version}" 171 ) 172 173 if args.commit_all == "": 174 raise ValueError("Commit message for args.commit_all cannot be empty.") 175 176 if args.publish and not hassle_utilities.on_primary_branch(): 177 print( 178 "WARNING: You are trying to publish a project that does not appear to be on its main branch." 179 ) 180 choice = input("Continue? (y/n) ") 181 if choice != "y": 182 print("Quitting hassle.") 183 sys.exit() 184 185 return args 186 187 188def build( 189 package_dir: Pathier, 190 skip_tests: bool = False, 191 overwrite_dependencies: bool = False, 192 increment_version: str | None = None, 193): 194 """Perform the build process. 195 196 Steps: 197 * Run tests (unless `skip_tests` is `True`) 198 * Raise error and abandon build if tests fail 199 * Format source code with `Black` 200 * Sort source code imports with `isort` 201 * Update project dependencies in `pyproject.toml` 202 * Increment version in `pyproject.toml` if `increment_version` supplied 203 * Generate docs 204 * Delete previous `dist` folder contents 205 * Invoke build module""" 206 if not skip_tests and not run_tests(package_dir): 207 raise RuntimeError( 208 f"ERROR: {package_dir.stem} failed testing.\nAbandoning build." 209 ) 210 hassle_utilities.format_files(package_dir) 211 [isort.file(path) for path in package_dir.rglob("*.py")] 212 hassle_utilities.update_dependencies( 213 package_dir / "pyproject.toml", overwrite_dependencies 214 ) 215 if increment_version: 216 hassle_utilities.increment_version( 217 package_dir / "pyproject.toml", increment_version 218 ) 219 # Vermin isn't taking into account the minimum version of dependencies. 220 # Removing from now and defaulting to >=3.10 221 # hassle_utilities.update_minimum_python_version(pyproject_path) 222 hassle_utilities.generate_docs(package_dir) 223 (package_dir / "dist").delete() 224 os.system(f"{sys.executable} -m build {package_dir}") 225 226 227def main(args: argparse.Namespace = None): 228 if not args: 229 args = get_args() 230 231 pyproject_path = args.package / "pyproject.toml" 232 args.package.mkcwd() 233 234 git = Git() 235 236 if not pyproject_path.exists(): 237 raise FileNotFoundError(f"Could not locate pyproject.toml for {args.package}") 238 239 if args.generate_tests: 240 generate_test_files(args.package) 241 242 if args.run_tests: 243 run_tests(args.package) 244 245 if args.build: 246 build( 247 args.package, 248 args.skip_tests, 249 args.overwrite_dependencies, 250 args.increment_version, 251 ) 252 253 if args.increment_version and not args.build: 254 hassle_utilities.increment_version(pyproject_path, args.increment_version) 255 256 if args.commit_all: 257 if args.commit_all == "build": 258 version = pyproject_path.loads()["project"]["version"] 259 args.commit_all = f"chore: build v{version}" 260 git.add() 261 git.commit(f'-m "{args.commit_all}"') 262 263 if args.tag_version: 264 hassle_utilities.tag_version(args.package) 265 266 if args.update_changelog: 267 hassle_utilities.update_changelog(pyproject_path) 268 if args.tag_version: 269 with git.capture_output(): 270 tags = git.tag("--sort=-committerdate") 271 most_recent_tag = tags[: tags.find("\n")] 272 git.execute(f"tag -d {most_recent_tag}") 273 input("Press enter to continue after manually adjusting the changelog...") 274 git.commit_files( 275 [str(args.package / "CHANGELOG.md")], "chore: update changelog" 276 ) 277 if args.tag_version: 278 with git.capture_output(): 279 git.tag(most_recent_tag) 280 281 if args.publish: 282 os.system(f"twine upload {args.package / 'dist' / '*'}") 283 284 if args.install: 285 os.system( 286 f"{sys.executable} -m pip install {args.package} --no-deps --upgrade --no-cache-dir" 287 ) 288 289 if args.sync: 290 git.pull(f"origin {git.current_branch} --tags") 291 git.push(f"origin {git.current_branch} --tags") 292 293 if args.is_published: 294 is_published = hassle_utilities.latest_version_is_published( 295 args.package / "pyproject.toml" 296 ) 297 if is_published: 298 print("The most recent version of this package has been published.") 299 else: 300 print("The most recent version of this package has not been published.") 301 302 303if __name__ == "__main__": 304 main(get_args())
def
get_args() -> argparse.Namespace:
17def get_args() -> argparse.Namespace: 18 parser = argparse.ArgumentParser() 19 20 parser.add_argument( 21 "package", 22 type=str, 23 default=".", 24 nargs="?", 25 help=""" The name of the package or project to use, 26 assuming it's a subfolder of your current working directory. 27 Can also be a full path to the package. If nothing is given, 28 the current working directory will be used.""", 29 ) 30 31 parser.add_argument( 32 "-b", "--build", action="store_true", help=""" Build the package. """ 33 ) 34 35 parser.add_argument( 36 "-t", 37 "--tag_version", 38 action="store_true", 39 help=""" Add a git tag corresponding to the version in pyproject.toml. """, 40 ) 41 42 parser.add_argument( 43 "-i", 44 "--install", 45 action="store_true", 46 help=""" Install the package from source. """, 47 ) 48 49 parser.add_argument( 50 "-iv", 51 "--increment_version", 52 type=str, 53 default=None, 54 choices=["major", "minor", "patch"], 55 help=""" Increment version in pyproject.toml. 56 Can be one of "major", "minor", or "patch". """, 57 ) 58 59 parser.add_argument( 60 "-p", 61 "--publish", 62 action="store_true", 63 help=""" Publish package to PyPi. 64 Note: You must have configured twine 65 and registered a PyPi account/generated an API 66 key to use this option.""", 67 ) 68 69 parser.add_argument( 70 "-rt", 71 "--run_tests", 72 action="store_true", 73 help=""" Run tests for the package. """, 74 ) 75 76 parser.add_argument( 77 "-gt", 78 "--generate_tests", 79 action="store_true", 80 help=""" Generate tests for the package. """, 81 ) 82 83 parser.add_argument( 84 "-uc", 85 "--update_changelog", 86 action="store_true", 87 help=""" Update changelog file. """, 88 ) 89 90 parser.add_argument( 91 "-od", 92 "--overwrite_dependencies", 93 action="store_true", 94 help=""" When building a package, packagelister will be used 95 to update the dependencies list in pyproject.toml. 96 The default behavior is to append any new dependencies to 97 the current list so as not to erase any manually added dependencies 98 that packagelister may not detect. If you don't have any manually 99 added dependencies and want to remove any dependencies that your 100 project no longer uses, pass this flag.""", 101 ) 102 103 parser.add_argument( 104 "-ca", 105 "--commit_all", 106 type=str, 107 default=None, 108 help=""" Git stage and commit all tracked files with this supplied commit message. 109 If 'build' is passed, all commits will have message: 'chore: build v{current_version}""", 110 ) 111 112 parser.add_argument( 113 "-s", 114 "--sync", 115 action="store_true", 116 help=""" Pull from github, then push current commit to repo. """, 117 ) 118 119 parser.add_argument( 120 "-dv", 121 "--dependency_versions", 122 action="store_true", 123 help=""" Include version specifiers for dependencies in 124 pyproject.toml.""", 125 ) 126 127 parser.add_argument( 128 "-up", 129 "--update", 130 type=str, 131 default=None, 132 choices=["major", "minor", "patch"], 133 help=""" Expects one argument: "major", "minor", or "patch". 134 Passing "-up minor" is equivalent to passing "--build --tag_version --increment_version minor --update_changelog --commit_all build --sync". 135 To publish the updated package, the -p/--publish switch needs to be added to the cli input. 136 To install the updated package, the -i/--install switch also needs to be added.""", 137 ) 138 139 parser.add_argument( 140 "-st", 141 "--skip_tests", 142 action="store_true", 143 help=""" Don't run tests when using the -b/--build command. """, 144 ) 145 146 parser.add_argument( 147 "-ip", 148 "--is_published", 149 action="store_true", 150 help=""" Check that the version number in `pyproject.toml` and `pypi.org/project/{project_name}` agree. """, 151 ) 152 153 args = parser.parse_args() 154 155 args.package = Pathier(args.package).resolve() 156 157 if args.update: 158 args.build = True 159 args.tag_version = True 160 args.increment_version = args.update 161 args.update_changelog = True 162 args.commit_all = "build" 163 args.sync = True 164 165 if args.increment_version and args.increment_version not in [ 166 "major", 167 "minor", 168 "patch", 169 ]: 170 raise ValueError( 171 f"Invalid option for -iv/--increment_version: {args.increment_version}" 172 ) 173 174 if args.commit_all == "": 175 raise ValueError("Commit message for args.commit_all cannot be empty.") 176 177 if args.publish and not hassle_utilities.on_primary_branch(): 178 print( 179 "WARNING: You are trying to publish a project that does not appear to be on its main branch." 180 ) 181 choice = input("Continue? (y/n) ") 182 if choice != "y": 183 print("Quitting hassle.") 184 sys.exit() 185 186 return args
def
build( package_dir: pathier.pathier.Pathier, skip_tests: bool = False, overwrite_dependencies: bool = False, increment_version: str | None = None):
189def build( 190 package_dir: Pathier, 191 skip_tests: bool = False, 192 overwrite_dependencies: bool = False, 193 increment_version: str | None = None, 194): 195 """Perform the build process. 196 197 Steps: 198 * Run tests (unless `skip_tests` is `True`) 199 * Raise error and abandon build if tests fail 200 * Format source code with `Black` 201 * Sort source code imports with `isort` 202 * Update project dependencies in `pyproject.toml` 203 * Increment version in `pyproject.toml` if `increment_version` supplied 204 * Generate docs 205 * Delete previous `dist` folder contents 206 * Invoke build module""" 207 if not skip_tests and not run_tests(package_dir): 208 raise RuntimeError( 209 f"ERROR: {package_dir.stem} failed testing.\nAbandoning build." 210 ) 211 hassle_utilities.format_files(package_dir) 212 [isort.file(path) for path in package_dir.rglob("*.py")] 213 hassle_utilities.update_dependencies( 214 package_dir / "pyproject.toml", overwrite_dependencies 215 ) 216 if increment_version: 217 hassle_utilities.increment_version( 218 package_dir / "pyproject.toml", increment_version 219 ) 220 # Vermin isn't taking into account the minimum version of dependencies. 221 # Removing from now and defaulting to >=3.10 222 # hassle_utilities.update_minimum_python_version(pyproject_path) 223 hassle_utilities.generate_docs(package_dir) 224 (package_dir / "dist").delete() 225 os.system(f"{sys.executable} -m build {package_dir}")
Perform the build process.
Steps:
- Run tests (unless
skip_tests
isTrue
) - Raise error and abandon build if tests fail
- Format source code with
Black
- Sort source code imports with
isort
- Update project dependencies in
pyproject.toml
- Increment version in
pyproject.toml
ifincrement_version
supplied - Generate docs
- Delete previous
dist
folder contents - Invoke build module
def
main(args: argparse.Namespace = None):
228def main(args: argparse.Namespace = None): 229 if not args: 230 args = get_args() 231 232 pyproject_path = args.package / "pyproject.toml" 233 args.package.mkcwd() 234 235 git = Git() 236 237 if not pyproject_path.exists(): 238 raise FileNotFoundError(f"Could not locate pyproject.toml for {args.package}") 239 240 if args.generate_tests: 241 generate_test_files(args.package) 242 243 if args.run_tests: 244 run_tests(args.package) 245 246 if args.build: 247 build( 248 args.package, 249 args.skip_tests, 250 args.overwrite_dependencies, 251 args.increment_version, 252 ) 253 254 if args.increment_version and not args.build: 255 hassle_utilities.increment_version(pyproject_path, args.increment_version) 256 257 if args.commit_all: 258 if args.commit_all == "build": 259 version = pyproject_path.loads()["project"]["version"] 260 args.commit_all = f"chore: build v{version}" 261 git.add() 262 git.commit(f'-m "{args.commit_all}"') 263 264 if args.tag_version: 265 hassle_utilities.tag_version(args.package) 266 267 if args.update_changelog: 268 hassle_utilities.update_changelog(pyproject_path) 269 if args.tag_version: 270 with git.capture_output(): 271 tags = git.tag("--sort=-committerdate") 272 most_recent_tag = tags[: tags.find("\n")] 273 git.execute(f"tag -d {most_recent_tag}") 274 input("Press enter to continue after manually adjusting the changelog...") 275 git.commit_files( 276 [str(args.package / "CHANGELOG.md")], "chore: update changelog" 277 ) 278 if args.tag_version: 279 with git.capture_output(): 280 git.tag(most_recent_tag) 281 282 if args.publish: 283 os.system(f"twine upload {args.package / 'dist' / '*'}") 284 285 if args.install: 286 os.system( 287 f"{sys.executable} -m pip install {args.package} --no-deps --upgrade --no-cache-dir" 288 ) 289 290 if args.sync: 291 git.pull(f"origin {git.current_branch} --tags") 292 git.push(f"origin {git.current_branch} --tags") 293 294 if args.is_published: 295 is_published = hassle_utilities.latest_version_is_published( 296 args.package / "pyproject.toml" 297 ) 298 if is_published: 299 print("The most recent version of this package has been published.") 300 else: 301 print("The most recent version of this package has not been published.")