~brenns10/subc

01b342c3e02eabeb3c146d192ceb54656e1fe5a1 — Stephen Brennan 1 year, 8 months ago a4bfed3
Replace comment-annotations with real ones (py36+)

This commit also swaps out some "pass" for docstrings, which brings test
coverage to 100% on the pytest-cov report. It's a bit weird that it has
to be that way, but whatever.
3 files changed, 48 insertions(+), 36 deletions(-)

M subc.py
M tests/test_basic.py
M tox.ini
M subc.py => subc.py +43 -25
@@ 4,12 4,13 @@ A simple sub-command library for writing rich CLIs
"""
import argparse
import collections
import typing as t
from abc import ABC
from abc import abstractproperty
from abc import abstractmethod


def _first_different(s1, s2):
def _first_different(s1: str, s2: str) -> int:
    """
    Return index of the first different character in s1 or s2. If the strings
    are the same, raises a ValueError.


@@ 22,7 23,7 @@ def _first_different(s1, s2):
    return i + 1


def _unique_prefixes(strings):
def _unique_prefixes(strings: t.Iterable[str]) -> t.Dict[str, t.List[str]]:
    """
    Helper to find a list of unique prefixes for each string in strings.



@@ 77,32 78,31 @@ class Command(ABC):
    """

    @abstractproperty
    def name(self):
        # type: () -> str
        pass
    def name(self) -> str:
        """A field or property which is used for the command name argument"""

    @abstractproperty
    def description(self):
        # type: () -> str
        pass
    def description(self) -> str:
        """A field or property which is used as the help/description"""

    def add_args(self, parser):
        # type: (argparse.ArgumentParser) -> None
    def add_args(self, parser: argparse.ArgumentParser):
        pass  # default is no arguments

    @abstractmethod
    def run(self):
        # type: () -> None
        pass
    def run(self) -> t.Any:
        """Function which is called for this command."""

    def base_run(self, args):
        # type: (argparse.Namespace) -> None
    def base_run(self, args: argparse.Namespace):
        self.args = args
        return self.run()

    @classmethod
    def add_commands(cls, parser, default=None, shortest_prefix=False):
        # type: (argparse.ArgumentParser) -> None
    def add_commands(
        cls,
        parser: argparse.ArgumentParser,
        default: t.Optional[str] = None,
        shortest_prefix: bool = False
    ) -> argparse.ArgumentParser:
        default_set = False
        subparsers = parser.add_subparsers(title='sub-command')
        subclasses = collections.deque(cls.__subclasses__())


@@ 133,19 133,37 @@ class Command(ABC):
                default_set = True

        if not default_set:
            def default(*args, **kwargs):
            def default_func(*args, **kwargs):
                raise Exception('you must select a sub-command')
            parser.set_defaults(func=default)
            parser.set_defaults(func=default_func)
        return parser

    @classmethod
    def parse_args(cls, description, default=None, args=None, shortest_prefix=False):
    def parse_args(
            cls,
            description,
            default: t.Optional[str] = None,
            args: t.Optional[t.List[str]] = None,
            shortest_prefix: bool = False,
    ) -> argparse.Namespace:
        parser = argparse.ArgumentParser(description=description)
        cls.add_commands(parser, default=default, shortest_prefix=shortest_prefix)
        cls.add_commands(
            parser, default=default, shortest_prefix=shortest_prefix,
        )
        return parser.parse_args(args)

    @classmethod
    def main(cls, description, default=None, args=None, shortest_prefix=False):
        # type: (str) -> None
        args = cls.parse_args(description, default=default, args=args, shortest_prefix=shortest_prefix)
        args.func(args)
    def main(
            cls,
            description: str,
            default: t.Optional[str] = None,
            args: t.Optional[t.List[str]] = None,
            shortest_prefix: bool = False,
    ) -> t.Any:
        ns = cls.parse_args(
            description,
            default=default,
            args=args,
            shortest_prefix=shortest_prefix
        )
        return ns.func(ns)

M tests/test_basic.py => tests/test_basic.py +4 -10
@@ 83,7 83,7 @@ def test_shortest_prefix_disabled():
        description = 'foo'

        def run(self):
            pass
            ''

    class FirstCmd(MyBase):
        name = 'first'


@@ 100,7 100,7 @@ def test_shortest_prefix_dup():
        description = 'foo'

        def run(self):
            pass
            ''

    class FirstCmd(MyBase):
        name = 'first'


@@ 133,14 133,11 @@ def test_default():


def test_nodefault():
    cmd = None

    class MyBase(Command):
        description = 'foo'

        def run(self):
            nonlocal cmd
            cmd = self
            ''

    class FirstCmd(MyBase):
        name = 'first'


@@ 153,14 150,11 @@ def test_nodefault():


def test_intermediate_not_included():
    cmd = None

    class MyBase(Command):
        description = 'foo'

        def run(self):
            nonlocal cmd
            cmd = self
            ''

    class Intermediate(MyBase):
        name = 'intermediate'

M tox.ini => tox.ini +1 -1
@@ 4,7 4,7 @@ envlist = py36,py39
[testenv]
deps = -rrequirements-dev.txt
commands =
    pytest tests --cov=subc --cov=tests {posargs} --cov-report html
    pytest tests --cov=subc --cov=tests {posargs} --cov-report term --cov-report html

#[testenv:docs]
#description = invoke sphinx-build to build the HTML docs