Skip to content

Commit

Permalink
Merge branch 'release/1.5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
glentner committed Jun 22, 2020
2 parents c649c73 + 3306de7 commit 494a867
Show file tree
Hide file tree
Showing 29 changed files with 1,684 additions and 272 deletions.
6 changes: 2 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@
.pytest_cache/
.vscode/
.idea/
.ropeproject/

# build files
*.egg-info/
build/
dist/
*.egg-info/
docs/_build/

# cache
__pycache__/

# pipenv
Pipfile.lock
.env
.env
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ install:
@pipenv install --dev

test:
@pipenv run pytest
@pipenv run pytest -v

dist:
@pipenv run setup.py sdist bdist_wheel
Expand Down
4 changes: 2 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]
cmdkit = {editable = true,path = "."}
toml = "*"
pyyaml = "*"
pytest = "*"
pylint = "*"
hypothesis = "*"
sphinx = "*"
sphinx-rtd-theme = "*"
ipython = "*"
twine = "*"
pydata-sphinx-theme = "*"

[packages]
logalpha = ">=2.0.2"
cmdkit = {editable = true, path = "."}

[requires]
python_version = "3.8"
560 changes: 560 additions & 0 deletions Pipfile.lock

Large diffs are not rendered by default.

48 changes: 0 additions & 48 deletions README.md

This file was deleted.

102 changes: 102 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
CmdKit
======

A library for developing command line utilities in Python.

.. image:: https://img.shields.io/badge/license-Apache-blue.svg?style=flat
:target: https://www.apache.org/licenses/LICENSE-2.0
:alt: License

.. image:: https://img.shields.io/pypi/v/cmdkit.svg?style=flat&color=blue
:target: https://pypi.org/project/cmdkit
:alt: PyPI Version

.. image:: https://img.shields.io/pypi/pyversions/cmdkit.svg?logo=python&logoColor=white&style=flat
:target: https://pypi.org/project/cmdkit
:alt: Python Versions

.. image:: https://readthedocs.org/projects/cmdkit/badge/?version=latest&style=flat
:target: https://cmdkit.readthedocs.io
:alt: Documentation

.. image:: https://pepy.tech/badge/cmdkit
:target: https://pepy.tech/badge/cmdkit
:alt: Downloads

|
The *cmdkit* library implements a few common patterns needed for commandline tools in Python.
It only touches a few concepts but it implements them well.
The idea is to reduce the boilerplate needed to get a full featured CLI off the ground.
Applications developed using *cmdkit* are easy to implement, easy to maintain, and easy to
understand.

|
-------------------

Features
--------

- An ``~cmdkit.cli.Interface`` class for parsing commandline arguments.
- An ``~cmdkit.app.Application`` class that provides the boilerplate for a good entry-point.
- Basic ``~cmdkit.logging``.
- A ``~cmdkit.config.Configuration`` class built on top of a ``~cmdkit.config.Namespace``
class that provides automatic depth-first merging of dictionaries from local files,
as well as automatic environment variable discovery.

|
-------------------

Installation
------------

*CmdKit* is built on Python 3.7+ and can be installed using Pip.

.. code-block:: none
➜ pip install cmdkit
|
-------------------

Getting Started
---------------

Checkout the `Tutorial <https://cmdkit.readthedocs.io/tutorial>`_ for examples.

You can also checkout how `cmdkit` is being used by other projects.

======================================================== =======================================================
Project Description
======================================================== =======================================================
`REFITT <https://github.com/refitt/refitt>`_ Recommender Engine for Intelligent Transient Tracking
`hyper-shell <https://github.com/glentner/hyper-shell>`_ Hyper-shell is an elegant, cross-platform, high-performance
computing utility for processing shell commands over a
distributed, asynchronous queue.
`delete-cli <https://github.com/glentner/delete-cli>`_ A simple, cross-platform, commandline move-to-trash.
======================================================== =======================================================

|
-------------------

Documentation
-------------

Documentation for getting started, the API, and common recipes are available at
`cmdkit.readthedocs.io <https://cmdkit.readthedocs.io>`_.

|
-------------------

Contributions
-------------

Contributions are welcome in the form of suggestions for additional features, pull requests with
new features or bug fixes, etc. If you find bugs or have questions, open an *Issue* here. If and
when the project grows, a code of conduct will be provided along side a more comprehensive set of
guidelines for contributing; until then, just be nice.
6 changes: 5 additions & 1 deletion cmdkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
"""Package initialization for CmdKit."""


from .__meta__ import (__pkgname__, __version__, __authors__, __contact__,\
__license__, __copyright__, __description__)

import sys
if sys.version_info < (3, 7):
raise SystemError('cmdkit requires at least Python 3.7 to run.')
raise SystemError('cmdkit requires at least Python 3.7 to run.')

2 changes: 1 addition & 1 deletion cmdkit/__meta__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


__pkgname__ = 'cmdkit'
__version__ = '1.4.0'
__version__ = '1.5.0'
__authors__ = 'Geoffrey Lentner'
__contact__ = '<[email protected]>'
__license__ = 'Apache License'
Expand Down
60 changes: 54 additions & 6 deletions cmdkit/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@
Application class implementation.
"""

# allow for return annotations
# type annotations
from __future__ import annotations
from typing import List, Dict, Callable, NamedTuple

# standard libs
import abc
from typing import NamedTuple, List, Dict, Callable


# internal libs
from . import cli
from . import config
from .logging import log


class exit_status:
class ExitStatus(NamedTuple):
"""Collection of exit status values."""
success: int = 0
usage: int = 1
Expand All @@ -36,17 +36,26 @@ class exit_status:
uncaught_exception: int = 6


# global shared instance
exit_status = ExitStatus()


class Application(abc.ABC):
"""
Abstract base class for all application interfaces.
An application is typically initialized with one of the factory methods
:func:`~from_namespace` or :func:`~from_cmdline`. These parse command line
arguments using the member :class:`~Interface`. Direct initialization takes
named parameters that are simple assigned to the instance. These should be
existing class-level attributes with annotations.
"""

interface: cli.Interface = None
ALLOW_NOARGS: bool = False

exceptions: Dict[Exception, Callable[[Exception], int]] = dict()
log_error: Callable[[str], None] = log.critical # pylint: disable=no-member

log_error: Callable[[str], None] = log.critical

def __init__(self, **parameters) -> None:
"""Direct initialization sets `parameters`."""
Expand Down Expand Up @@ -90,6 +99,7 @@ def main(cls, cmdline: List[str] = None) -> int:

except cli.ArgumentError as error:
cls.log_error(error)

return exit_status.bad_argument

except KeyboardInterrupt:
Expand All @@ -115,3 +125,41 @@ def __enter__(self) -> Application:
def __exit__(self, *exc) -> None:
"""Release resources."""
pass


class CompletedCommand(Exception):
"""Contains the exit status of a member application's main method."""


class ApplicationGroup(Application):
"""
A group entry-point delegates to member `Applications`.
"""

interface: cli.Interface = None
commands: Dict[str, Application] = None
command: str = None
cmdline: List[str] = None

exceptions = {
CompletedCommand: (lambda cmd: int(cmd.args[0]))
}

@classmethod
def from_cmdline(cls, cmdline: List[str] = None) -> Application:
"""Initialize via command line arguments (e.g., `sys.argv`)."""
if not cmdline:
return super().from_cmdline(cmdline)
else:
first, *remainder = cmdline
self = super().from_cmdline([first])
self.cmdline = list(remainder)
return self

def run(self) -> None:
"""Delegate to member application."""
if self.command in self.commands:
status = self.commands[self.command].main(self.cmdline)
raise CompletedCommand(status)
else:
raise cli.ArgumentError(f'unrecognized command: {self.command}')
14 changes: 6 additions & 8 deletions cmdkit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,19 @@ class takes a pre-constructed usage and help string and uses those instead.


class HelpOption(Exception):
"""Raised when the `-h|--help` flag is given."""
"""Raised by :class:`~Interface` when the help option is passed."""

class VersionOption(Exception):
"""Raised when the `--version` flag is given."""
"""Raised by :class:`~Interface` whenever ``action='version'``."""

class ArgumentError(Exception):
"""Exceptions originating from `argparse`."""
"""Raised by :class:`~Interface` on bad arguments."""


# override version action to raise instead
def _version_action(self, parser, namespace, values, option_string=None) -> None:
raise VersionOption(self.version if self.version is not None else parser.version)


# override version action to raise exception
_argparse._VersionAction.__call__ = _version_action
_argparse._VersionAction.__call__ = _version_action # noqa (protected)


class Interface(_argparse.ArgumentParser):
Expand All @@ -51,7 +49,7 @@ class Interface(_argparse.ArgumentParser):
Example:
>>> from cmdkit.cli import Interface
>>> interface = Interface('my_app', 'usage: ...', 'help: ...')
>>> interface = Interface('myapp', 'usage: myapp ...', 'help: ...')
>>> interface.add_argument('--verbose', action='store_true')
"""

Expand Down
Loading

0 comments on commit 494a867

Please sign in to comment.