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()
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        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.

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        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.

@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        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.

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

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

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

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

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

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

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

Invoke pytest -s with Coverage.

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