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()
class HassleShell(argshell.argshell.ArgShell):
 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.

HassleShell(command: str, *args, **kwargs)
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.

@argshell.with_parser(parsers.get_add_script_parser)
def do_add_script(self, args: argshell.argshell.Namespace):
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.

@argshell.with_parser(parsers.get_build_parser)
def do_build(self, args: argshell.argshell.Namespace):
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.

def do_check_pypi(self, name: str):
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.

def do_config(self, _: str = ''):
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.

@argshell.with_parser(parsers.get_edit_config_parser)
def do_configure(self, args: argshell.argshell.Namespace):
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.

def do_format(self, _: str = ''):
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.

def do_is_published(self, _: str = ''):
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.

@argshell.with_parser(parsers.get_new_project_parser, [parsers.add_default_source_files])
def do_new(self, args: argshell.argshell.Namespace):
 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.

def do_publish(self, _: str = ''):
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.

def do_test(self, _: str):
148    def do_test(self, _: str):
149        """Invoke `pytest -s` with `Coverage`."""
150        utilities.run_tests()

Invoke pytest -s with Coverage.

@argshell.with_parser(parsers.get_update_parser)
def do_update(self, args: argshell.argshell.Namespace):
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
def main():
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_)