hassle.hassle_cli
1import subprocess 2import sys 3 4import argshell 5import pip 6from gitbetter import Git 7from pathier import Pathier 8 9from hassle import parsers, utilities 10from hassle.models import HassleConfig, HassleProject, Pyproject 11 12root = Pathier(__file__).parent 13 14 15class HassleShell(argshell.ArgShell): 16 def __init__(self, command: str, *args, **kwargs): 17 super().__init__(*args, **kwargs) 18 if command == "new": 19 # load a blank HassleProject 20 self.project = HassleProject(Pyproject.from_template(), Pathier.cwd(), []) 21 elif command != "check_pypi": 22 try: 23 self.project = HassleProject.load(Pathier.cwd()) 24 except Exception as e: 25 print(f"{Pathier.cwd().stem} does not appear to be a Hassle project.") 26 print(e) 27 28 def _build(self, args: argshell.Namespace): 29 self.project.format_source_files() 30 self.project.update_dependencies( 31 args.overwrite_dependencies, args.include_versions 32 ) 33 self.project.generate_docs() 34 self.project.distdir.delete() 35 self.project.save() 36 subprocess.run([sys.executable, "-m", "build", Pathier.cwd()]) 37 38 @argshell.with_parser(parsers.get_add_script_parser) 39 def do_add_script(self, args: argshell.Namespace): 40 """Add a script to the `pyproject.toml` file.""" 41 self.project.add_script(args.name, args.file.strip(".py"), args.function) 42 self.project.save() 43 44 @argshell.with_parser(parsers.get_build_parser) 45 def do_build(self, args: argshell.Namespace): 46 """Build this project.""" 47 if not args.skip_tests and not utilities.run_tests(): 48 raise RuntimeError( 49 f"ERROR: {Pathier.cwd().stem} failed testing.\nAbandoning build." 50 ) 51 self._build(args) 52 53 def do_check_pypi(self, name: str): 54 """Check if the given package name is taken on pypi.org or not.""" 55 name = name.strip('"') 56 if utilities.check_pypi(name): 57 print(f"{name} is already taken.") 58 else: 59 print(f"{name} is available.") 60 61 def do_config(self, _: str = ""): 62 """Print hassle config to terminal.""" 63 config = root / "hassle_config.toml" 64 if config.exists(): 65 print(config.read_text()) 66 else: 67 print("hassle_config.toml doesn't exist.") 68 69 @argshell.with_parser(parsers.get_edit_config_parser) 70 def do_configure(self, args: argshell.Namespace): 71 """Edit or create `hassle_config.toml`.""" 72 HassleConfig.configure( 73 args.name, args.email, args.github_username, args.docs_url, args.tag_prefix 74 ) 75 76 def do_format(self, _: str = ""): 77 """Format all `.py` files with `isort` and `black`.""" 78 self.project.format_source_files() 79 80 def do_is_published(self, _: str = ""): 81 """Check if the most recent version of this package is published to PYPI.""" 82 text = f"The most recent version of '{self.project.name}'" 83 if self.project.latest_version_is_published(): 84 print(f"{text} has been published.") 85 else: 86 print(f"{text} has not been published.") 87 88 @argshell.with_parser( 89 parsers.get_new_project_parser, 90 [parsers.add_default_source_files], 91 ) 92 def do_new(self, args: argshell.Namespace): 93 """Create a new project.""" 94 # Check if this name is taken. 95 if not args.not_package and utilities.check_pypi(args.name): 96 print(f"{args.name} already exists on pypi.org") 97 if not utilities.get_answer("Continue anyway?"): 98 sys.exit() 99 # Check if targetdir already exists 100 targetdir = Pathier.cwd() / args.name 101 if targetdir.exists(): 102 print(f"'{args.name}' already exists.") 103 if not utilities.get_answer("Overwrite?"): 104 sys.exit() 105 # Load config 106 if not HassleConfig.exists(): 107 HassleConfig.warn() 108 if not utilities.get_answer( 109 "Continue creating new package with blank config?" 110 ): 111 raise Exception("Aborting new package creation") 112 else: 113 print("Creating blank hassle_config.toml...") 114 HassleConfig.configure() 115 self.project = HassleProject.new( 116 targetdir, 117 args.name, 118 args.description, 119 args.dependencies, 120 args.keywords, 121 args.source_files, 122 args.add_script, 123 args.no_license, 124 ) 125 # If not a package (just a project) move source code to top level. 126 if args.not_package: 127 for file in self.project.srcdir.iterdir(): 128 file.copy(self.project.projectdir / file.name) 129 self.project.srcdir.parent.delete() 130 # Initialize Git 131 self.project.projectdir.mkcwd() 132 git = Git() 133 git.new_repo() 134 135 def do_publish(self, _: str = ""): 136 """Publish this package. 137 138 You must have 'twine' installed and set up to use this command.""" 139 if not utilities.on_primary_branch(): 140 print( 141 "WARNING: You are trying to publish a project that does not appear to be on its main branch." 142 ) 143 print(f"You are on branch '{Git().current_branch}'") 144 if not utilities.get_answer("Continue anyway?"): 145 return 146 subprocess.run(["twine", "upload", self.project.distdir / "*"]) 147 148 def do_test(self, _: str): 149 """Invoke `pytest -s` with `Coverage`.""" 150 utilities.run_tests() 151 152 @argshell.with_parser(parsers.get_update_parser) 153 def do_update(self, args: argshell.Namespace): 154 """Update this package.""" 155 if not args.skip_tests and not utilities.run_tests(): 156 raise RuntimeError( 157 f"ERROR: {Pathier.cwd().stem} failed testing.\nAbandoning build." 158 ) 159 self.project.bump_version(args.update_type) 160 self.project.save() 161 self._build(args) 162 git = Git() 163 if HassleConfig.exists(): 164 tag_prefix = HassleConfig.load().git.tag_prefix 165 else: 166 HassleConfig.warn() 167 print("Assuming no tag prefix.") 168 tag_prefix = "" 169 tag = f"{tag_prefix}{self.project.version}" 170 git.add_files([self.project.distdir, self.project.docsdir]) 171 git.add(". -u") 172 git.commit(f'-m "chore: build {tag}"') 173 # 'auto-changelog' generates based off of commits between tags 174 # So to include the changelog in the tagged commit, 175 # we have to tag the code, update/commit the changelog, delete the tag, and then retag 176 # (One of these days I'll just write my own changelog generator) 177 git.tag(tag) 178 self.project.update_changelog() 179 with git.capturing_output(): 180 git.tag(f"-d {tag}") 181 input("Press enter to continue after editing the changelog...") 182 git.add_files([self.project.changelog_path]) 183 git.commit_files([self.project.changelog_path], "chore: update changelog") 184 with git.capturing_output(): 185 git.tag(tag) 186 # Sync with remote 187 sync = f"origin {git.current_branch} --tags" 188 git.pull(sync) 189 git.push(sync) 190 if args.publish: 191 self.do_publish() 192 if args.install: 193 pip.main(["install", "."]) 194 195 196def main(): 197 """ """ 198 command = "" if len(sys.argv) < 2 else sys.argv[1] 199 shell = HassleShell(command) 200 if command == "help" and len(sys.argv) == 3: 201 input_ = f"help {sys.argv[2]}" 202 # Doing this so args that are multi-word strings don't get interpreted as separate args. 203 elif command: 204 input_ = f"{command} " + " ".join([f'"{arg}"' for arg in sys.argv[2:]]) 205 else: 206 input_ = "help" 207 shell.onecmd(input_) 208 209 210if __name__ == "__main__": 211 main()
16class HassleShell(argshell.ArgShell): 17 def __init__(self, command: str, *args, **kwargs): 18 super().__init__(*args, **kwargs) 19 if command == "new": 20 # load a blank HassleProject 21 self.project = HassleProject(Pyproject.from_template(), Pathier.cwd(), []) 22 elif command != "check_pypi": 23 try: 24 self.project = HassleProject.load(Pathier.cwd()) 25 except Exception as e: 26 print(f"{Pathier.cwd().stem} does not appear to be a Hassle project.") 27 print(e) 28 29 def _build(self, args: argshell.Namespace): 30 self.project.format_source_files() 31 self.project.update_dependencies( 32 args.overwrite_dependencies, args.include_versions 33 ) 34 self.project.generate_docs() 35 self.project.distdir.delete() 36 self.project.save() 37 subprocess.run([sys.executable, "-m", "build", Pathier.cwd()]) 38 39 @argshell.with_parser(parsers.get_add_script_parser) 40 def do_add_script(self, args: argshell.Namespace): 41 """Add a script to the `pyproject.toml` file.""" 42 self.project.add_script(args.name, args.file.strip(".py"), args.function) 43 self.project.save() 44 45 @argshell.with_parser(parsers.get_build_parser) 46 def do_build(self, args: argshell.Namespace): 47 """Build this project.""" 48 if not args.skip_tests and not utilities.run_tests(): 49 raise RuntimeError( 50 f"ERROR: {Pathier.cwd().stem} failed testing.\nAbandoning build." 51 ) 52 self._build(args) 53 54 def do_check_pypi(self, name: str): 55 """Check if the given package name is taken on pypi.org or not.""" 56 name = name.strip('"') 57 if utilities.check_pypi(name): 58 print(f"{name} is already taken.") 59 else: 60 print(f"{name} is available.") 61 62 def do_config(self, _: str = ""): 63 """Print hassle config to terminal.""" 64 config = root / "hassle_config.toml" 65 if config.exists(): 66 print(config.read_text()) 67 else: 68 print("hassle_config.toml doesn't exist.") 69 70 @argshell.with_parser(parsers.get_edit_config_parser) 71 def do_configure(self, args: argshell.Namespace): 72 """Edit or create `hassle_config.toml`.""" 73 HassleConfig.configure( 74 args.name, args.email, args.github_username, args.docs_url, args.tag_prefix 75 ) 76 77 def do_format(self, _: str = ""): 78 """Format all `.py` files with `isort` and `black`.""" 79 self.project.format_source_files() 80 81 def do_is_published(self, _: str = ""): 82 """Check if the most recent version of this package is published to PYPI.""" 83 text = f"The most recent version of '{self.project.name}'" 84 if self.project.latest_version_is_published(): 85 print(f"{text} has been published.") 86 else: 87 print(f"{text} has not been published.") 88 89 @argshell.with_parser( 90 parsers.get_new_project_parser, 91 [parsers.add_default_source_files], 92 ) 93 def do_new(self, args: argshell.Namespace): 94 """Create a new project.""" 95 # Check if this name is taken. 96 if not args.not_package and utilities.check_pypi(args.name): 97 print(f"{args.name} already exists on pypi.org") 98 if not utilities.get_answer("Continue anyway?"): 99 sys.exit() 100 # Check if targetdir already exists 101 targetdir = Pathier.cwd() / args.name 102 if targetdir.exists(): 103 print(f"'{args.name}' already exists.") 104 if not utilities.get_answer("Overwrite?"): 105 sys.exit() 106 # Load config 107 if not HassleConfig.exists(): 108 HassleConfig.warn() 109 if not utilities.get_answer( 110 "Continue creating new package with blank config?" 111 ): 112 raise Exception("Aborting new package creation") 113 else: 114 print("Creating blank hassle_config.toml...") 115 HassleConfig.configure() 116 self.project = HassleProject.new( 117 targetdir, 118 args.name, 119 args.description, 120 args.dependencies, 121 args.keywords, 122 args.source_files, 123 args.add_script, 124 args.no_license, 125 ) 126 # If not a package (just a project) move source code to top level. 127 if args.not_package: 128 for file in self.project.srcdir.iterdir(): 129 file.copy(self.project.projectdir / file.name) 130 self.project.srcdir.parent.delete() 131 # Initialize Git 132 self.project.projectdir.mkcwd() 133 git = Git() 134 git.new_repo() 135 136 def do_publish(self, _: str = ""): 137 """Publish this package. 138 139 You must have 'twine' installed and set up to use this command.""" 140 if not utilities.on_primary_branch(): 141 print( 142 "WARNING: You are trying to publish a project that does not appear to be on its main branch." 143 ) 144 print(f"You are on branch '{Git().current_branch}'") 145 if not utilities.get_answer("Continue anyway?"): 146 return 147 subprocess.run(["twine", "upload", self.project.distdir / "*"]) 148 149 def do_test(self, _: str): 150 """Invoke `pytest -s` with `Coverage`.""" 151 utilities.run_tests() 152 153 @argshell.with_parser(parsers.get_update_parser) 154 def do_update(self, args: argshell.Namespace): 155 """Update this package.""" 156 if not args.skip_tests and not utilities.run_tests(): 157 raise RuntimeError( 158 f"ERROR: {Pathier.cwd().stem} failed testing.\nAbandoning build." 159 ) 160 self.project.bump_version(args.update_type) 161 self.project.save() 162 self._build(args) 163 git = Git() 164 if HassleConfig.exists(): 165 tag_prefix = HassleConfig.load().git.tag_prefix 166 else: 167 HassleConfig.warn() 168 print("Assuming no tag prefix.") 169 tag_prefix = "" 170 tag = f"{tag_prefix}{self.project.version}" 171 git.add_files([self.project.distdir, self.project.docsdir]) 172 git.add(". -u") 173 git.commit(f'-m "chore: build {tag}"') 174 # 'auto-changelog' generates based off of commits between tags 175 # So to include the changelog in the tagged commit, 176 # we have to tag the code, update/commit the changelog, delete the tag, and then retag 177 # (One of these days I'll just write my own changelog generator) 178 git.tag(tag) 179 self.project.update_changelog() 180 with git.capturing_output(): 181 git.tag(f"-d {tag}") 182 input("Press enter to continue after editing the changelog...") 183 git.add_files([self.project.changelog_path]) 184 git.commit_files([self.project.changelog_path], "chore: update changelog") 185 with git.capturing_output(): 186 git.tag(tag) 187 # Sync with remote 188 sync = f"origin {git.current_branch} --tags" 189 git.pull(sync) 190 git.push(sync) 191 if args.publish: 192 self.do_publish() 193 if args.install: 194 pip.main(["install", "."])
Subclass this to create custom ArgShells.
17 def __init__(self, command: str, *args, **kwargs): 18 super().__init__(*args, **kwargs) 19 if command == "new": 20 # load a blank HassleProject 21 self.project = HassleProject(Pyproject.from_template(), Pathier.cwd(), []) 22 elif command != "check_pypi": 23 try: 24 self.project = HassleProject.load(Pathier.cwd()) 25 except Exception as e: 26 print(f"{Pathier.cwd().stem} does not appear to be a Hassle project.") 27 print(e)
Instantiate a line-oriented interpreter framework.
The optional argument 'completekey' is the readline name of a completion key; it defaults to the Tab key. If completekey is not None and the readline module is available, command completion is done automatically. The optional arguments stdin and stdout specify alternate input and output file objects; if not specified, sys.stdin and sys.stdout are used.
39 @argshell.with_parser(parsers.get_add_script_parser) 40 def do_add_script(self, args: argshell.Namespace): 41 """Add a script to the `pyproject.toml` file.""" 42 self.project.add_script(args.name, args.file.strip(".py"), args.function) 43 self.project.save()
Add a script to the pyproject.toml
file.
45 @argshell.with_parser(parsers.get_build_parser) 46 def do_build(self, args: argshell.Namespace): 47 """Build this project.""" 48 if not args.skip_tests and not utilities.run_tests(): 49 raise RuntimeError( 50 f"ERROR: {Pathier.cwd().stem} failed testing.\nAbandoning build." 51 ) 52 self._build(args)
Build this project.
54 def do_check_pypi(self, name: str): 55 """Check if the given package name is taken on pypi.org or not.""" 56 name = name.strip('"') 57 if utilities.check_pypi(name): 58 print(f"{name} is already taken.") 59 else: 60 print(f"{name} is available.")
Check if the given package name is taken on pypi.org or not.
62 def do_config(self, _: str = ""): 63 """Print hassle config to terminal.""" 64 config = root / "hassle_config.toml" 65 if config.exists(): 66 print(config.read_text()) 67 else: 68 print("hassle_config.toml doesn't exist.")
Print hassle config to terminal.
70 @argshell.with_parser(parsers.get_edit_config_parser) 71 def do_configure(self, args: argshell.Namespace): 72 """Edit or create `hassle_config.toml`.""" 73 HassleConfig.configure( 74 args.name, args.email, args.github_username, args.docs_url, args.tag_prefix 75 )
Edit or create hassle_config.toml
.
77 def do_format(self, _: str = ""): 78 """Format all `.py` files with `isort` and `black`.""" 79 self.project.format_source_files()
Format all .py
files with isort
and black
.
81 def do_is_published(self, _: str = ""): 82 """Check if the most recent version of this package is published to PYPI.""" 83 text = f"The most recent version of '{self.project.name}'" 84 if self.project.latest_version_is_published(): 85 print(f"{text} has been published.") 86 else: 87 print(f"{text} has not been published.")
Check if the most recent version of this package is published to PYPI.
89 @argshell.with_parser( 90 parsers.get_new_project_parser, 91 [parsers.add_default_source_files], 92 ) 93 def do_new(self, args: argshell.Namespace): 94 """Create a new project.""" 95 # Check if this name is taken. 96 if not args.not_package and utilities.check_pypi(args.name): 97 print(f"{args.name} already exists on pypi.org") 98 if not utilities.get_answer("Continue anyway?"): 99 sys.exit() 100 # Check if targetdir already exists 101 targetdir = Pathier.cwd() / args.name 102 if targetdir.exists(): 103 print(f"'{args.name}' already exists.") 104 if not utilities.get_answer("Overwrite?"): 105 sys.exit() 106 # Load config 107 if not HassleConfig.exists(): 108 HassleConfig.warn() 109 if not utilities.get_answer( 110 "Continue creating new package with blank config?" 111 ): 112 raise Exception("Aborting new package creation") 113 else: 114 print("Creating blank hassle_config.toml...") 115 HassleConfig.configure() 116 self.project = HassleProject.new( 117 targetdir, 118 args.name, 119 args.description, 120 args.dependencies, 121 args.keywords, 122 args.source_files, 123 args.add_script, 124 args.no_license, 125 ) 126 # If not a package (just a project) move source code to top level. 127 if args.not_package: 128 for file in self.project.srcdir.iterdir(): 129 file.copy(self.project.projectdir / file.name) 130 self.project.srcdir.parent.delete() 131 # Initialize Git 132 self.project.projectdir.mkcwd() 133 git = Git() 134 git.new_repo()
Create a new project.
136 def do_publish(self, _: str = ""): 137 """Publish this package. 138 139 You must have 'twine' installed and set up to use this command.""" 140 if not utilities.on_primary_branch(): 141 print( 142 "WARNING: You are trying to publish a project that does not appear to be on its main branch." 143 ) 144 print(f"You are on branch '{Git().current_branch}'") 145 if not utilities.get_answer("Continue anyway?"): 146 return 147 subprocess.run(["twine", "upload", self.project.distdir / "*"])
Publish this package.
You must have 'twine' installed and set up to use this command.
149 def do_test(self, _: str): 150 """Invoke `pytest -s` with `Coverage`.""" 151 utilities.run_tests()
Invoke pytest -s
with Coverage
.
153 @argshell.with_parser(parsers.get_update_parser) 154 def do_update(self, args: argshell.Namespace): 155 """Update this package.""" 156 if not args.skip_tests and not utilities.run_tests(): 157 raise RuntimeError( 158 f"ERROR: {Pathier.cwd().stem} failed testing.\nAbandoning build." 159 ) 160 self.project.bump_version(args.update_type) 161 self.project.save() 162 self._build(args) 163 git = Git() 164 if HassleConfig.exists(): 165 tag_prefix = HassleConfig.load().git.tag_prefix 166 else: 167 HassleConfig.warn() 168 print("Assuming no tag prefix.") 169 tag_prefix = "" 170 tag = f"{tag_prefix}{self.project.version}" 171 git.add_files([self.project.distdir, self.project.docsdir]) 172 git.add(". -u") 173 git.commit(f'-m "chore: build {tag}"') 174 # 'auto-changelog' generates based off of commits between tags 175 # So to include the changelog in the tagged commit, 176 # we have to tag the code, update/commit the changelog, delete the tag, and then retag 177 # (One of these days I'll just write my own changelog generator) 178 git.tag(tag) 179 self.project.update_changelog() 180 with git.capturing_output(): 181 git.tag(f"-d {tag}") 182 input("Press enter to continue after editing the changelog...") 183 git.add_files([self.project.changelog_path]) 184 git.commit_files([self.project.changelog_path], "chore: update changelog") 185 with git.capturing_output(): 186 git.tag(tag) 187 # Sync with remote 188 sync = f"origin {git.current_branch} --tags" 189 git.pull(sync) 190 git.push(sync) 191 if args.publish: 192 self.do_publish() 193 if args.install: 194 pip.main(["install", "."])
Update this package.
Inherited Members
- argshell.argshell.ArgShell
- do_quit
- do_sys
- do_reload
- do_help
- cmdloop
- emptyline
- cmd.Cmd
- precmd
- postcmd
- preloop
- postloop
- parseline
- onecmd
- default
- completedefault
- completenames
- complete
- get_names
- complete_help
- print_topics
- columnize
197def main(): 198 """ """ 199 command = "" if len(sys.argv) < 2 else sys.argv[1] 200 shell = HassleShell(command) 201 if command == "help" and len(sys.argv) == 3: 202 input_ = f"help {sys.argv[2]}" 203 # Doing this so args that are multi-word strings don't get interpreted as separate args. 204 elif command: 205 input_ = f"{command} " + " ".join([f'"{arg}"' for arg in sys.argv[2:]]) 206 else: 207 input_ = "help" 208 shell.onecmd(input_)