Skip to content

Commit

Permalink
Ensure full test coverage (#163)
Browse files Browse the repository at this point in the history
* feat: deprecate outdated constant

* docs: list deprecated items

* test: cover argh.assembling.add_subcommands()

* test: cover overrides in add_commands()

* test: 100% coverage for assembling and interaction

* feat: deprecate `safe_input()`

The function used to be helpful in the py2/py3 world with all the bytes
vs Unicode strings mess.  In 2023 this is not relevant anymore.

* docs: deprecation of `dispatch(..., pre_call=...)`

This is not in fact deprecation per se but merely an attempt to draw
attention to a fact that was mentioned almost ten years ago in the
discussion (#63): the `pre_call` argument in `dispatch()` is not a
feature but a hack which is not recommended and will be removed.

This commit makes it clear when precisely it is going to be removed and
what actions are expected from the user.

* test: 100% coverage of dispatching

* chore: drop the old encoding declaration

Let's just agree that it's 2023 and move on.

* test: 100% coverage of completion

* test: require 100% coverage
  • Loading branch information
neithere authored Feb 15, 2023
1 parent 313698b commit 2cbed69
Show file tree
Hide file tree
Showing 21 changed files with 497 additions and 76 deletions.
39 changes: 38 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,43 @@ Backward incompatible changes:

- Dropped support for Python 2.7 and 3.7.

Deprecated features, to be removed in v.0.30:

- `argh.assembling.SUPPORTS_ALIASES`.

- Always `True` for recent versions of Python.

- `argh.io.safe_input()` AKA `argh.interaction.safe_input()`.

- Not relevant anymore. Please use the built-in `input()` instead.

- argument `pre_call` in `dispatch()`.

Even though this hack seems to have been used in some projects, it was never
part of the official API and never recommended.

Describing your use case in the `discussion about shared arguments`_ can
help improve the library to accomodate it in a proper way.

.. _discussion about shared arguments: https://github.com/neithere/argh/issues/63

- Argument help as annotations.

- Annotations will only be used for types after v.0.30.
- Please replace any instance of::

def func(foo: "Foobar"):

with the following::

@arg('-f', '--foo', help="Foobar")
def func(foo):

It will be decided later how to keep this functionality "DRY" (don't repeat
yourself) without conflicts with modern conventions and tools.

- Added deprecation warnings for some arguments deprecated back in v.0.26.

Version 0.27.2
--------------

Expand Down Expand Up @@ -98,7 +135,7 @@ really outdated, please read this list carefully and grep your code.
`argh.completion.autocomplete()`. Debug-level logging is used instead.
(The warnings were deprecated since v.0.25).

Some more stuff has been scheduled **to be purged before 1.0**:
Deprecated:

- Deprecated arguments `title`, `help` and `description` in `add_commands()`
helper function. See documentation and issue #60.
Expand Down
28 changes: 10 additions & 18 deletions src/argh/assembling.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand All @@ -14,7 +13,6 @@
Functions and classes to properly assemble your commands in a parser.
"""
import argparse
import warnings
from collections import OrderedDict

Expand All @@ -39,21 +37,16 @@
]


def _check_support_aliases():
p = argparse.ArgumentParser()
s = p.add_subparsers()
try:
s.add_parser("x", aliases=[])
except TypeError:
return False
else:
return True
# TODO: remove in v.0.30.
SUPPORTS_ALIASES = True
"""
.. deprecated:: 0.28.0
This constant will be removed in Argh v.0.30.
It's not relevant anymore because it's always `True` for all Python
versions currently supported by Argh.
SUPPORTS_ALIASES = _check_support_aliases()
"""
Calculated on load. If `True`, current version of argparse supports
alternative command names (can be set via :func:`~argh.decorators.aliases`).
"""


Expand Down Expand Up @@ -475,9 +468,8 @@ def _extract_command_meta_from_func(func):
"formatter_class": PARSER_FORMATTER,
}

# try adding aliases for command name
if SUPPORTS_ALIASES:
func_parser_kwargs["aliases"] = getattr(func, ATTR_ALIASES, [])
# add aliases for command name
func_parser_kwargs["aliases"] = getattr(func, ATTR_ALIASES, [])

return cmd_name, func_parser_kwargs

Expand Down
9 changes: 5 additions & 4 deletions src/argh/completion.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand Down Expand Up @@ -69,9 +68,9 @@ def func(...):
try:
import argcomplete
except ImportError:
pass
argcomplete = None
else:
COMPLETION_ENABLED = True
COMPLETION_ENABLED = True # pragma: no cover


__all__ = ["autocomplete", "COMPLETION_ENABLED"]
Expand All @@ -90,4 +89,6 @@ def autocomplete(parser):
if COMPLETION_ENABLED:
argcomplete.autocomplete(parser)
elif "bash" in os.getenv("SHELL", ""):
logger.debug("Bash completion not available. Install argcomplete.")
logger.debug("Bash completion is not available. Please install argcomplete.")
else:
pass
1 change: 0 additions & 1 deletion src/argh/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand Down
1 change: 0 additions & 1 deletion src/argh/decorators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand Down
24 changes: 18 additions & 6 deletions src/argh/dispatching.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand Down Expand Up @@ -223,10 +222,18 @@ def _execute_command(function, namespace_obj, errors_file, pre_call=None):
All other exceptions propagate unless marked as wrappable
by :func:`wrap_errors`.
"""
if pre_call: # XXX undocumented because I'm unsure if it's OK
# Actually used in real projects:
# * https://google.com/search?q=argh+dispatch+pre_call
# * https://github.com/neithere/argh/issues/63
# TODO: remove in v.0.30
if pre_call: # pragma: no cover
# This is NOT a documented and recommended API.
# The common use case for this hack is to inject shared arguments.
# Such approach would promote an approach which is not in line with the
# purpose of the library, i.e. to keep things natural and "pythonic".
# Argh is about keeping CLI in line with function signatures.
# The `pre_call` hack effectively destroys this mapping.
# There should be a better solution, e.g. decorators and/or some shared
# objects.
#
# See discussion here: https://github.com/neithere/argh/issues/63
pre_call(namespace_obj)

# the function is nested to catch certain exceptions (see below)
Expand Down Expand Up @@ -308,7 +315,12 @@ def dispatch_command(function, *args, **kwargs):
set_default_command(parser, foo)
dispatch(parser)
This function can be also used as a decorator.
This function can be also used as a decorator::
@dispatch_command
def main(foo=123):
return foo + 1
"""
parser = argparse.ArgumentParser(formatter_class=PARSER_FORMATTER)
set_default_command(parser, function)
Expand Down
1 change: 0 additions & 1 deletion src/argh/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand Down
1 change: 0 additions & 1 deletion src/argh/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand Down
6 changes: 3 additions & 3 deletions src/argh/interaction.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand All @@ -12,6 +11,7 @@
Interaction
~~~~~~~~~~~
"""
# TODO: remove in v.0.30
from argh.io import safe_input

__all__ = ["confirm", "safe_input"]
Expand Down Expand Up @@ -67,10 +67,10 @@ def delete(key, silent=False):
if default is None:
cnt = 1
while not choice and cnt < MAX_ITERATIONS:
choice = safe_input(prompt)
choice = input(prompt)
cnt += 1
else:
choice = safe_input(prompt)
choice = input(prompt)
except KeyboardInterrupt:
return None
if choice in ("yes", "y", "Y"):
Expand Down
14 changes: 6 additions & 8 deletions src/argh/io.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand All @@ -15,16 +14,15 @@
__all__ = ["safe_input"]


def _input(prompt):
# this function can be mocked up in tests
return input(prompt)
def safe_input(prompt): # pragma: no cover
"""
.. deprecated:: 0.28
This function will be removed in Argh v.0.30.
Please use the built-in function `input()` instead.
def safe_input(prompt):
"""
Prompts user for input. Correctly handles prompt message encoding.
"""
if not isinstance(prompt, str):
prompt = prompt.decode()

return _input(prompt)
return input(prompt)
16 changes: 15 additions & 1 deletion src/argh/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
#
# Copyright © 2010—2023 Andrey Mikhaylenko and contributors
#
Expand All @@ -14,6 +13,7 @@
"""
import argparse
import inspect
import re


def get_subparsers(parser, create=False):
Expand Down Expand Up @@ -54,3 +54,17 @@ def get_arg_spec(function):
if inspect.ismethod(function):
spec = spec._replace(args=spec.args[1:])
return spec


def unindent(text):
"""
Given a multi-line string, decreases indentation of all lines so that the
first non-empty line has zero indentation and the remaining lines are
adjusted accordingly.
"""
match = re.match("(^|\n)( +)", text)
if not match:
return text
first_line_indentation = match.group(2)
depth = len(first_line_indentation)
return re.sub(rf"(^|\n) {{{depth}}}", "\\1", text)
1 change: 0 additions & 1 deletion tests/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
"""
Common stuff for tests
~~~~~~~~~~~~~~~~~~~~~~
Expand Down
35 changes: 34 additions & 1 deletion tests/test_assembling.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
"""
Unit Tests For Assembling Phase
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -290,3 +289,37 @@ def test_set_default_command_deprecation_warnings():
DeprecationWarning, match="Argument `help` is deprecated in add_commands()"
):
argh.add_commands(parser, [], namespace="c", help="bar")


@mock.patch("argh.assembling.add_commands")
def test_add_subcommands(mock_add_commands):
mock_parser = mock.MagicMock()

def get_items():
pass

argh.add_subcommands(
mock_parser,
"db",
[get_items],
title="database commands",
help="CRUD for our silly database",
)

mock_add_commands.assert_called_with(
mock_parser,
[get_items],
namespace="db",
namespace_kwargs={
"title": "database commands",
"help": "CRUD for our silly database",
},
)


@mock.patch("argh.helpers.autocomplete")
def test_arghparser_autocomplete_method(mock_autocomplete):
p = argh.ArghParser()
p.autocomplete()

mock_autocomplete.assert_called()
47 changes: 47 additions & 0 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Unit Tests For Autocompletion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
"""
from unittest.mock import patch

import argh


@patch("argh.completion.COMPLETION_ENABLED", True)
@patch("argh.completion.argcomplete")
def test_enabled(mock_argcomplete):
parser = argh.ArghParser()

parser.autocomplete()

mock_argcomplete.autocomplete.assert_called_with(parser)


@patch("argh.completion.COMPLETION_ENABLED", False)
@patch("argh.completion.argcomplete")
@patch("argh.completion.logger")
def test_disabled_without_bash(mock_logger, mock_argcomplete):
parser = argh.ArghParser()

parser.autocomplete()

mock_argcomplete.assert_not_called()
mock_logger.debug.assert_not_called()


@patch("argh.completion.COMPLETION_ENABLED", False)
@patch("argh.completion.argcomplete")
@patch("argh.completion.os.getenv")
@patch("argh.completion.logger")
def test_disabled_with_bash(mock_logger, mock_getenv, mock_argcomplete):
mock_getenv.return_value = "/bin/bash"
parser = argh.ArghParser()

parser.autocomplete()

mock_argcomplete.assert_not_called()
mock_getenv.assert_called_with("SHELL", "")
mock_logger.debug.assert_called_with(
"Bash completion is not available. Please install argcomplete."
)
1 change: 0 additions & 1 deletion tests/test_decorators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding: utf-8
"""
Unit Tests For Decorators
~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
Loading

0 comments on commit 2cbed69

Please sign in to comment.