hassle.hassle_cli API documentation

# hassle.hassle_cli

View Source

      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): View Source

     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) View Source

    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): View Source

    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): View Source

    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): View Source

    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 = ''): View Source

    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): View Source

    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 = ''): View Source

    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 = ''): View Source

    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): View Source

     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 = ''): View Source

    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): View Source

    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): View Source

    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(): View Source

    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_)