Skip to content

Commit

Permalink
Improvements4 (#200)
Browse files Browse the repository at this point in the history
* doc strings according to PEP
* skipped tests marked properly
* refactored CLI
* added autocompletion
* cleanup
* switch github actions from checkout@v1 to checkout@v2
* fixed codecov name in github actions
  • Loading branch information
jkowalleck authored Feb 20, 2020
1 parent ba0695c commit d1b8155
Show file tree
Hide file tree
Showing 28 changed files with 321 additions and 227 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
steps:
- name: Fetch Code
# see https://github.com/actions/checkout
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Set up Python ${{ matrix.pyhon_version }}
# see https://github.com/actions/setup-python
uses: actions/setup-python@v1
Expand All @@ -42,5 +42,5 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml
flags: tests
name: od:${{ matrix.os }} py:${{ matrix.pyhon }}
name: "os:${{ matrix.os }} py:${{ matrix.pyhon }}"
yml: ./codecov.yml
4 changes: 2 additions & 2 deletions .github/workflows/pythonpublish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
steps:
- name: Fetch Code
# see https://github.com/actions/checkout
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Set up Python
# see https://github.com/actions/setup-python
uses: actions/setup-python@v1
Expand All @@ -37,7 +37,7 @@ jobs:
steps:
- name: Fetch Code
# see https://github.com/actions/checkout
uses: actions/checkout@v1
uses: actions/checkout@v2
- name: Set up Python
# see https://github.com/actions/setup-python
uses: actions/setup-python@v1
Expand Down
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
* fixed issue [#187](https://github.com/k4cg/nichtparasoup/issues/187).
* pinned test dependencies to greater/equal current latest minor version.

### Added

* commandline autocompletion via [`argcomplete`](https://pypi.org/project/argcomplete/).


## 2.3.1

Expand Down
4 changes: 2 additions & 2 deletions nichtparasoup/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sys

from nichtparasoup.cmdline import main
from nichtparasoup.cli.main import main as _main

sys.exit(main())
sys.exit(_main())
11 changes: 9 additions & 2 deletions nichtparasoup/_internals/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
__all__ = ['_logger', '_log', '_message', '_message_exception', '_confirm']

"""
yes, everything is underscored.
its internal foo that is not for public use.
Yes, everything is underscored.
Its internal foo that is not for public use.
"""

import logging
Expand Down Expand Up @@ -30,6 +31,12 @@ def _log(level: _LOG_LEVEL, message: str, *args: Any, **kwargs: Any) -> None:
getattr(_logger, level)(message.rstrip(), *args, **kwargs)


def _logging_init(level: int) -> None: # pragma: no cover
if not logging.root.handlers:
logging.root.setLevel(level)
logging.root.addHandler(logging.StreamHandler())


def _message(message: str, color: Optional[str] = None, file: Optional[TextIO] = None) -> None:
from sys import stdout
newline = '\r\n'
Expand Down
4 changes: 4 additions & 0 deletions nichtparasoup/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""Subpackage containing all of nichtparasoup's command line interface related code
"""

# This file intentionally does not import submodules
42 changes: 21 additions & 21 deletions nichtparasoup/cmdline/__init__.py → nichtparasoup/cli/commands.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import logging
from typing import Any, List, Optional
from typing import Any, Optional

from nichtparasoup._internals import _message, _message_exception


def _logging_init(level: int) -> None: # pragma: no cover
if not logging.root.handlers:
logging.root.setLevel(level)
logging.root.addHandler(logging.StreamHandler())


class Commands(object):

@staticmethod
def run(config_file: Optional[str] = None) -> int:
from os.path import abspath
from nichtparasoup._internals import _logging_init
from nichtparasoup.config import get_config, get_imagecrawler
from nichtparasoup.core import NPCore
from nichtparasoup.core.server import Server as ImageServer
Expand All @@ -35,7 +30,12 @@ def run(config_file: Optional[str] = None) -> int:
return 1

@classmethod
def config(cls, action: str, config_file: str) -> int:
def config(cls, **actions: Any) -> int:
active_actions = dict((k, v) for k, v in actions.items() if v)
if len(active_actions) != 1:
_message_exception(ValueError('exactly one action required'))
return 255
action, config_file = active_actions.popitem()
return dict(
check=cls.config_check_file,
dump=cls.config_dump_file,
Expand All @@ -44,10 +44,10 @@ def config(cls, action: str, config_file: str) -> int:
@staticmethod
def config_dump_file(config_file: str) -> int:
from os.path import abspath, isfile
from nichtparasoup._internals import _confirm
from nichtparasoup.config import dump_defaults
config_file = abspath(config_file)
if isfile(config_file):
from nichtparasoup._internals import _confirm
overwrite = _confirm('File already exists, overwrite?')
if overwrite is not True:
_message('Abort.')
Expand Down Expand Up @@ -80,20 +80,20 @@ def info(cls, **actions: Any) -> int:
_message_exception(ValueError('exactly one action required'))
return 255
action, action_value = active_actions.popitem()
return dict(
return dict( # type: ignore
version=cls.info_version,
imagecrawler_list=cls.info_imagecrawler_list,
imagecrawler_desc=cls.info_imagecrawler_desc,
)[action](action_value)

@staticmethod
def info_version(_: Any) -> int:
def info_version(_: Optional[Any] = None) -> int:
from nichtparasoup import VERSION
_message(VERSION)
return 0

@staticmethod
def info_imagecrawler_list(_: Any) -> int:
def info_imagecrawler_list(_: Optional[Any] = None) -> int:
from nichtparasoup.imagecrawler import get_imagecrawlers
imagecrawlers = get_imagecrawlers().names()
if not imagecrawlers:
Expand Down Expand Up @@ -133,12 +133,12 @@ def info_imagecrawler_desc(imagecrawler: str) -> int:
]))
return 0


def main(args: Optional[List[str]] = None) -> int:
from nichtparasoup.cmdline.argparse import parser as argparser
options = dict(argparser.parse_args(args=args).__dict__)
if options.pop('debug', False):
_logging_init(logging.DEBUG)
_message('DEBUG ENABLED :)', 'cyan')
command = options.pop('command')
return getattr(Commands, command)(**options) # type: ignore
@staticmethod
def completion(shell: str) -> int:
from sys import stdout
from argcomplete import shellcode # type: ignore
stdout.write(shellcode(
['nichtparasoup'], shell=shell,
use_defaults=True, complete_arguments=None,
))
return 0
24 changes: 24 additions & 0 deletions nichtparasoup/cli/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# PYTHON_ARGCOMPLETE_OK

__all__ = ["main"]

import logging
from typing import List, Optional

from argcomplete import autocomplete # type: ignore

from nichtparasoup._internals import _logging_init, _message
from nichtparasoup.cli.commands import Commands
from nichtparasoup.cli.parser import create_parser


def main(args: Optional[List[str]] = None) -> int: # pragma: no cover
parser = create_parser()
autocomplete(parser, always_complete_options='long')
options = dict(parser.parse_args(args=args).__dict__)
del parser
if options.pop('debug', False):
_logging_init(logging.DEBUG)
_message('DEBUG ENABLED :)', 'cyan')
command = options.pop('command')
return getattr(Commands, command)(**options) # type: ignore
120 changes: 120 additions & 0 deletions nichtparasoup/cli/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
__all__ = ["create_parser"]

from argparse import ArgumentParser
from typing import Any, Set

from argcomplete import FilesCompleter # type: ignore

from nichtparasoup.imagecrawler import get_imagecrawlers


def imagecrawler_completion(*args: Any, **kwargs: Any) -> Set[str]:
return set(get_imagecrawlers().names())


yaml_file_completion = FilesCompleter('yaml', 'yml')


def create_parser() -> ArgumentParser: # pragma: no cover
# used `__tmp_action` several times, to omit type-checkers warning ala 'Action has no attribute "completer"'

parser = ArgumentParser(
add_help=True,
allow_abbrev=False,
)

parser.add_argument(
'--debug',
help='Enable debug output.',
action='store_true', dest="debug",
)

commands = parser.add_subparsers(
title='Commands',
metavar='<command>',
dest='command',
)
commands.required = True

command_run = commands.add_parser(
'run',
help='run a server',
description='Start a web-server to display random images.',
add_help=True,
allow_abbrev=False,
)
__tmp_action = command_run.add_argument(
'-c', '--use-config',
help='Use a YAML config file instead of the defaults.',
metavar='<file>',
action='store', dest="config_file", type=str,
)
__tmp_action.completer = yaml_file_completion # type: ignore
del __tmp_action

command_config = commands.add_parser(
'config',
description='Get config related things done.',
help='Config related functions.',
add_help=True,
allow_abbrev=False,
)
command_config_switches = command_config.add_mutually_exclusive_group(required=True)
__tmp_action = command_config_switches.add_argument(
'--check',
help='Validate and probe a YAML config file;',
metavar='<file>',
action='store', dest='check', type=str,
)
__tmp_action.completer = yaml_file_completion # type: ignore
del __tmp_action
command_config_switches.add_argument(
'--dump',
help='Dump YAML config into a file;',
metavar='<file>',
action='store', dest='dump', type=str,
)

command_info = commands.add_parser(
'info',
description='Get info for several topics.',
help='Get info for several topics.',
add_help=True,
allow_abbrev=False,
)
command_info_switches = command_info.add_mutually_exclusive_group(required=True)
command_info_switches.add_argument(
'--imagecrawler-list',
help='List available image crawler types.',
action='store_true', dest='imagecrawler_list',
)
__tmp_action = command_info_switches.add_argument(
'--imagecrawler-desc',
help='Describe an image crawler type and its config.',
metavar='<crawler>',
action='store', dest='imagecrawler_desc', type=str,
)
__tmp_action.completer = imagecrawler_completion # type: ignore
del __tmp_action
command_info_switches.add_argument(
'--version',
help="Show program's version number.",
action='store_true', dest='version',
)

command_completion = commands.add_parser(
'completion',
description='Helper command used for command completion.',
epilog='Autocompletion is powered by https://pypi.org/project/argcomplete/',
help='Helper command to be used for command completion.',
add_help=True,
allow_abbrev=False,
)
command_completion.add_argument(
'-s', '--shell',
help='Emit completion code for the specified shell.',
action='store', dest='shell', type=str, required=True,
choices=('bash', 'tcsh', 'fish'),
)

return parser
Loading

0 comments on commit d1b8155

Please sign in to comment.