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

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

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

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

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

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

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

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

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

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