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 else: 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 if utilities.check_pypi(name): 56 print(f"{name} is already taken.") 57 else: 58 print(f"{name} is available.") 59 60 def do_config(self, _: str = ""): 61 """Print hassle config to terminal.""" 62 config = root / "hassle_config.toml" 63 if config.exists(): 64 print(config.read_text()) 65 else: 66 print("hassle_config.toml doesn't exist.") 67 68 @argshell.with_parser(parsers.get_edit_config_parser) 69 def do_configure(self, args: argshell.Namespace): 70 """Edit or create `hassle_config.toml`.""" 71 HassleConfig.configure( 72 args.name, args.email, args.github_username, args.docs_url, args.tag_prefix 73 ) 74 75 def do_format(self, _: str = ""): 76 """Format all `.py` files with `isort` and `black`.""" 77 self.project.format_source_files() 78 79 def do_is_published(self, _: str = ""): 80 """Check if the most recent version of this package is published to PYPI.""" 81 text = f"The most recent version of '{self.project.name}'" 82 if self.project.latest_version_is_published(): 83 print(f"{text} has been published.") 84 else: 85 print(f"{text} has not been published.") 86 87 @argshell.with_parser( 88 parsers.get_new_project_parser, 89 [parsers.add_default_source_files], 90 ) 91 def do_new(self, args: argshell.Namespace): 92 """Create a new project.""" 93 # Check if this name is taken. 94 if not args.not_package and utilities.check_pypi(args.name): 95 print(f"{args.name} already exists on pypi.org") 96 if not utilities.get_answer("Continue anyway?"): 97 sys.exit() 98 # Check if targetdir already exists 99 targetdir = Pathier.cwd() / args.name 100 if targetdir.exists(): 101 print(f"'{args.name}' already exists.") 102 if not utilities.get_answer("Overwrite?"): 103 sys.exit() 104 # Load config 105 if not HassleConfig.exists(): 106 HassleConfig.warn() 107 if not utilities.get_answer( 108 "Continue creating new package with blank config?" 109 ): 110 raise Exception("Aborting new package creation") 111 else: 112 print("Creating blank hassle_config.toml...") 113 HassleConfig.configure() 114 self.project = HassleProject.new( 115 targetdir, 116 args.name, 117 args.description, 118 args.dependencies, 119 args.keywords, 120 args.source_files, 121 args.add_script, 122 args.no_license, 123 ) 124 # If not a package (just a project) move source code to top level. 125 if args.not_package: 126 for file in self.project.srcdir.iterdir(): 127 file.copy(self.project.projectdir / file.name) 128 self.project.srcdir.parent.delete() 129 # Initialize Git 130 self.project.projectdir.mkcwd() 131 git = Git() 132 git.new_repo() 133 134 def do_publish(self, _: str = ""): 135 """Publish this package. 136 137 You must have 'twine' installed and set up to use this command.""" 138 if not utilities.on_primary_branch(): 139 print( 140 "WARNING: You are trying to publish a project that does not appear to be on its main branch." 141 ) 142 print(f"You are on branch '{Git().current_branch}'") 143 if not utilities.get_answer("Continue anyway?"): 144 return 145 subprocess.run(["twine", "upload", self.project.distdir / "*"]) 146 147 def do_test(self, _: str): 148 """Invoke `pytest -s` with `Coverage`.""" 149 utilities.run_tests() 150 151 @argshell.with_parser(parsers.get_update_parser) 152 def do_update(self, args: argshell.Namespace): 153 """Update this package.""" 154 if not args.skip_tests and not utilities.run_tests(): 155 raise RuntimeError( 156 f"ERROR: {Pathier.cwd().stem} failed testing.\nAbandoning build." 157 ) 158 self.project.bump_version(args.update_type) 159 self.project.save() 160 self._build(args) 161 git = Git() 162 if HassleConfig.exists(): 163 tag_prefix = HassleConfig.load().git.tag_prefix 164 else: 165 HassleConfig.warn() 166 print("Assuming no tag prefix.") 167 tag_prefix = "" 168 tag = f"{tag_prefix}{self.project.version}" 169 git.add_files([self.project.distdir, self.project.docsdir]) 170 git.add(". -u") 171 git.commit(f'-m "chore: build {tag}"') 172 # 'auto-changelog' generates based off of commits between tags 173 # So to include the changelog in the tagged commit, 174 # we have to tag the code, update/commit the changelog, delete the tag, and then retag 175 # (One of these days I'll just write my own changelog generator) 176 git.tag(tag) 177 self.project.update_changelog() 178 with git.capturing_output(): 179 git.tag(f"-d {tag}") 180 input("Press enter to continue after editing the changelog...") 181 git.add_files([self.project.changelog_path]) 182 git.commit_files([self.project.changelog_path], "chore: update changelog") 183 with git.capturing_output(): 184 git.tag(tag) 185 # Sync with remote 186 sync = f"origin {git.current_branch} --tags" 187 git.pull(sync) 188 git.push(sync) 189 if args.publish: 190 self.do_publish() 191 if args.install: 192 pip.main(["install", "."]) 193 194 195def main(): 196 """ """ 197 command = "" if len(sys.argv) < 2 else sys.argv[1] 198 shell = HassleShell(command) 199 if command == "help" and len(sys.argv) == 3: 200 input_ = f"help {sys.argv[2]}" 201 # Doing this so args that are multi-word strings don't get interpreted as separate args. 202 elif command: 203 input_ = f"{command} " + " ".join([f'"{arg}"' for arg in sys.argv[2:]]) 204 else: 205 input_ = "help" 206 shell.onecmd(input_) 207 208 209if __name__ == "__main__": 210 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 else: 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 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", "."])
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 else: 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 if utilities.check_pypi(name): 57 print(f"{name} is already taken.") 58 else: 59 print(f"{name} is available.")
Check if the given package name is taken on pypi.org or not.
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.")
Print hassle config to terminal.
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 )
Edit or create hassle_config.toml
.
76 def do_format(self, _: str = ""): 77 """Format all `.py` files with `isort` and `black`.""" 78 self.project.format_source_files()
Format all .py
files with isort
and black
.
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.")
Check if the most recent version of this package is published to PYPI.
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()
Create a new project.
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 / "*"])
Publish this package.
You must have 'twine' installed and set up to use this command.
148 def do_test(self, _: str): 149 """Invoke `pytest -s` with `Coverage`.""" 150 utilities.run_tests()
Invoke pytest -s
with Coverage
.
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", "."])
Update this package.
Inherited Members
- argshell.argshell.ArgShell
- do_quit
- do_sys
- do_help
- cmdloop
- emptyline
- cmd.Cmd
- precmd
- postcmd
- preloop
- postloop
- parseline
- onecmd
- default
- completedefault
- completenames
- complete
- get_names
- complete_help
- print_topics
- columnize
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_)