Skip to content

Commit

Permalink
Provide tab completion using Click
Browse files Browse the repository at this point in the history
* Add script to generate and deploy click-based completions

The herewith added script creates a tab-completion script for the Bash
and Zsh shells and puts it in the system folder. The created completion
script relies on the used click framework for command line parsing.

* Remove completion files for Bash and Zsh

When switching to the tab-completion capabilities provided by the click
command line parsing framework, the manually created and maintained
completion files are not necessary anymore.

* Add tab completion for projects

The subcommands 'aggregate', 'log', and 'report' now have tab completion
support for projects.

* Add tab completion for tags

The subcommands 'aggregate', 'log', and 'report' now have tab completion
support for tags.

* Factor out initialisation of watson

The line initialising watson read some environment variables and thus
was not trivial. Therefore its duplications are against the DRY
principle. In order to fix this, the line was moved to a helper
function.

* Add autocompletion for frame ids

For the commands 'edit', 'remove' and 'restart' now the frame id is
completed on tab.

* StyleFix Do not return generator; yield instead

* Add tab-completion for type in `watson rename`

* Add tab completion for projects/tags in 'rename'

With this commit, Click is able to generate completions for projects and
tags in `watson rename`.

* Provide completion for projects and tasks in `start` and `add`

This commit adds Bash completion capabilities for projects and tasks in
the subcommands `watson start` and `watson add`. For this, it imitates
parsing of the complete command line, just as the aforementioned
subcommands do.

* Move autocompletion helper to separate module

* Add dummy test for autocompletion module

* Add watson fixture using custom frames list

A fixture is added which simply loads a custom frames list. This can be
used as a starting point for more useful tests.

* Factor out TEST_FIXTURE_DIR

This object was used twice, so it can live in the test utility module.

* Improve docstring of get_frames()

* Add and use frames file for autocompletion tests

* Add basic test for get_frames()

This very basic test to get_frames sets up all the logic that is
required to run the autocompletion helpers in test mode. The main
problem was that the helpers need to start their own instance of
Watson. Therefore, the existing fixtures could not be used.

* CleanUp Get rid of old watson fixture

The old watson fixture was deleted. The tested functions start their own
Watson instance, relying on the environment variable WATSON_DIR.
Therefore, the fixture is not needed anymore.

* Switch to new test frames file

A new frames file for autocompletion tests is deployed. The most
prominent benefit is that it is much longer now, giving room for better
tests.

The one existing user was adapted slightly, albeit the test certainly
needs some extra robustness.

* Add serious tests for get_frames()

Tests for get_frames() are added. Different to the previous ones, the
actually test the behaviour of get_frames for three plausible use cases.

* Add tests for get_projects()

The tests are copies of the tests for get_frames() and adapted to fit
the projects.

* Add tests for get_rename_type()

The tests are copies of the tests for get_projects() and adapted to fit
the rename types.

* CleanUp Merge tests for distinct returns

* CleanUp Merge tests for completion of empty prefix

* CleanUp Merge tests for completion of nonexisting prefix

* CleanUp Merge tests for completion of existing prefix

* Add test for known completion values

* Add get_tags() to test suite

* Add get_rename_name() to test suite

* Add context argument to parameterize

This prepares the addition of get_rename_name().

* Add args argument to pytest.parameterize

This prepares the addition of another test case for
get_project_or_task().

* Change args parameter from None to []

The parameter args is a list of strings, so it is plausible to create it
as such. Furthermore, this allows the addition of a test case for
get_project_or_task_completion().

* Test project completion in get_project_or_task()

* Test tag completion in get_project_or_task()

* StyleFix Adapt code formatting using black

The format of the code was a bit messed up. It was once reformatted
using black in order to improve the readability a bit.

* Minor improvements on docstrings

* StyleFix Apply black autoformator on autocompletion module

* CleanUp Move magic numbers to constants

A lot of magic numbers were used to specify how many results were
expected. This violates established practice. Therefore, almost all of
them were replaced by constants.

* CleanUp Turn prepare_sysenv_for_testing() to fixture

The aforementioned function is turned to be a fixture and attached to
the tests depending on it. This simplifies the tests a bit, as the test
function signature and the test body become more lightweight.

* Simplify autocompletion tests

Previously, when Watson was called for autocompleting, two Watson
objects were created: first in "cli.py:cli()" and again in the
corresponding "autocomplete.py" method.

Changes:
- Move TEST_FIXTURE_DIR to "tests/__init.py__" as conftest is more
suitable for fixtures (which are automatically available).
- Add new watson_df fixture which easily allows to override the
configuration path using pytest-datafiles.
- Move frame test file into its own folder, avoiding manual renames.
- Avoid needing CTXDummy object using SimpleNamespace and watson_df.
- Rename get_watson_instance() to create_watson() to avoid implying that
it's a singleton.

Signed-off-by: Max Görner <[email protected]>

* Move script for completion deployment to scripts/

Besides just moving it, it is renamed to reflect what will be the
intended purpose after a pending refactoring.

* Stop deploying completion receipts.

In an effort to adapt the code to the existing documentation, the script
is rewritten such that it does not deploy the generated receipts.
Instead, it will write them to a file.

* Remove redundant check for -h|--help

* Integrate create-completion-script.sh in Makefile

Unfortunately, generating the completion receipts has exit code 1 for
unknown reasons. Thus, a '|| true' is required.

* Explain completion receipt generation in documentation

* Tighten silencing an expected error

Instead of suppressing an expected error in the Makefile, it now is
suppressed exactly where it is caused in the
create-completion-script.sh.

* BugFix Work around Click bug in all functions

The commit by David Alfonso improved the code style considerably.
Unfortunately it also triggered a bug in Click that does not pass the
context object properly.

While a workaround was added in some functions, some others have been
forgotten. This caused some attempts on autocompletion to fail.

The commit at hand applies the workaround everywhere where it is needed.

* StyleFix Apply Black auto formatter for proper formatting

Reference PR (#276)
  • Loading branch information
MaxG87 authored and jmaupetit committed Nov 29, 2019
1 parent 224bf44 commit c3a1a4d
Show file tree
Hide file tree
Showing 13 changed files with 480 additions and 405 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@ mostlyclean: clean distclean
docs: install-dev
$(PYTHON) scripts/gen-cli-docs.py
mkdocs build

.PHONY: completion-scripts
completion-scripts:
scripts/create-completion-script.sh bash
scripts/create-completion-script.sh zsh
10 changes: 10 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,18 @@ $ python setup.py install

### Command line completion

#### Generating Completion Receipts

The completion receipts for both Bash and ZSH can be generated by

make completion-scripts

However, this receipt requires to have `watson` available in your PATH
environment variable.

#### Bash


If you use a Bash-compatible shell, you can install the `watson.completion` file from the source distribution as `/etc/bash.completion.d/watson` - or wherever your distribution keeps the Bash completion configuration files. After you restart your shell, you can then just type `watson` on your command line and then hit `TAB` to see all available commands. Depending on your input, it completes `watson` commands, command options, projects, tags and frame IDs.

#### ZSH
Expand Down
44 changes: 44 additions & 0 deletions scripts/create-completion-script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash

set -euo pipefail

function print_help() {

cat <<EOF
Usage: $0 shell-type
This script generates the auto completion receipt required by Bash or Zsh.
Since the generated receipt is only a wrapper around the click framework, this
results in correct tab completion, regardless of the currently used version
watson.
The argument shell-type must be either "bash" or "zsh".
EOF
}

# Parse command line parameters
if [[ $# -ne 1 ]]
then
echo "Please provide exactly one input argument." >&2
exit 1
fi

case $1 in
-h|--help)
print_help
exit 0
;;
bash)
src_command="source"
target_file="watson.completion"
;;
zsh)
src_command="source_zsh"
target_file="watson.zsh-completion"
;;
*)
echo "Unknown argument '$1'. Please consult help text." >&2
exit 1
esac

_WATSON_COMPLETE=$src_command watson > "$target_file" || true
10 changes: 10 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Utility functions for the unit tests."""

import os
import datetime

try:
Expand All @@ -12,6 +13,15 @@
except ImportError:
from io import StringIO

import py


TEST_FIXTURE_DIR = py.path.local(
os.path.dirname(
os.path.realpath(__file__)
)
) / 'resources'


def mock_datetime(dt, dt_module):

Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ def watson(config_dir):
@pytest.fixture
def runner():
return CliRunner()


@pytest.fixture
def watson_df(datafiles):
"""Creates a Watson object with datafiles in config directory."""
return Watson(config_dir=str(datafiles))
102 changes: 102 additions & 0 deletions tests/resources/autocompletion/frames
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
[
[
1539683175,
1539685649,
"project1",
"41dcffb7bd74442794b9385c3e8891fc",
[
"tag1"
],
1539685649
],
[
1546588591,
1546589427,
"project2",
"e8b53f1dda684672806e0f347d2b11fc",
[
"tag2"
],
1546589427
],
[
1550065199,
1550068200,
"project1",
"ef9933131f254b6fa94dda2a85107195",
[
"tag1"
],
1550069493
],
[
1550150400,
1550152117,
"project3-A",
"f4f78aa79744440b9cbd28edef1ba0b0",
[
"tag2"
],
1550152117
],
[
1550829600,
1550831642,
"project3-A",
"10c6ff8612c84b239b5141cd04f10415",
[
"tag2"
],
1550831642
],
[
1551954000,
1551956768,
"project1",
"e113e26dbf8d4db3ba6361a73709ffd6",
[
"tag2"
],
1551956768
],
[
1552489966,
1552492200,
"project2",
"d5185c8e811a40efbad43d2ff775f5e8",
[
"tag2"
],
1552549848
],
[
1553507114,
1553509735,
"project3-B",
"379f567a9d584498aa8729a170b8b8ad",
[
"tag2"
],
1553509735
],
[
1555050600,
1555052400,
"project4",
"f4f7429d70454175bb87ce2254bbd925",
[
"tag3"
],
1555063210
],
[
1556615369,
1556616625,
"project3-B",
"af9fe637030a465ba279abc3c1441b66",
[
"tag3"
],
1556616626
]
]
148 changes: 148 additions & 0 deletions tests/test_autocompletion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Unit tests for the 'autocompletion' module."""

import json
from argparse import Namespace

import pytest

from watson.autocompletion import (
get_frames,
get_project_or_task_completion,
get_projects,
get_rename_name,
get_rename_types,
get_tags,
)

from . import TEST_FIXTURE_DIR


AUTOCOMPLETION_FRAMES_PATH = TEST_FIXTURE_DIR / "autocompletion"
with open(str(AUTOCOMPLETION_FRAMES_PATH / "frames")) as fh:
N_FRAMES = len(json.load(fh))
N_PROJECTS = 5
N_TASKS = 3
N_VARIATIONS_OF_PROJECT3 = 2
N_FRAME_IDS_FOR_PREFIX = 2

ClickContext = Namespace


@pytest.mark.datafiles(AUTOCOMPLETION_FRAMES_PATH)
@pytest.mark.parametrize(
"func_to_test, rename_type, args",
[
(get_frames, None, []),
(get_project_or_task_completion, None, ["project1", "+tag1"]),
(get_project_or_task_completion, None, []),
(get_projects, None, []),
(get_rename_name, "project", []),
(get_rename_name, "tag", []),
(get_rename_types, None, []),
(get_tags, None, []),
],
)
def test_if_returned_values_are_distinct(
watson_df, func_to_test, rename_type, args
):
ctx = ClickContext(obj=watson_df, params={"rename_type": rename_type})
prefix = ""
ret_list = list(func_to_test(ctx, args, prefix))
assert sorted(ret_list) == sorted(set(ret_list))


@pytest.mark.datafiles(AUTOCOMPLETION_FRAMES_PATH)
@pytest.mark.parametrize(
"func_to_test, n_expected_returns, rename_type, args",
[
(get_frames, N_FRAMES, None, []),
(get_project_or_task_completion, N_TASKS, None, ["project1", "+"]),
(get_project_or_task_completion, N_PROJECTS, None, []),
(get_projects, N_PROJECTS, None, []),
(get_rename_name, N_PROJECTS, "project", []),
(get_rename_name, N_TASKS, "tag", []),
(get_rename_types, 2, None, []),
(get_tags, N_TASKS, None, []),
],
)
def test_if_empty_prefix_returns_everything(
watson_df, func_to_test, n_expected_returns, rename_type, args
):
prefix = ""
ctx = ClickContext(obj=watson_df, params={"rename_type": rename_type})
completed_vals = set(func_to_test(ctx, args, prefix))
assert len(completed_vals) == n_expected_returns


@pytest.mark.datafiles(AUTOCOMPLETION_FRAMES_PATH)
@pytest.mark.parametrize(
"func_to_test, rename_type, args",
[
(get_frames, None, []),
(get_project_or_task_completion, None, ["project1", "+"]),
(get_project_or_task_completion, None, ["project1", "+tag1", "+"]),
(get_project_or_task_completion, None, []),
(get_projects, None, []),
(get_rename_name, "project", []),
(get_rename_name, "tag", []),
(get_rename_types, None, []),
(get_tags, None, []),
],
)
def test_completion_of_nonexisting_prefix(
watson_df, func_to_test, rename_type, args
):
ctx = ClickContext(obj=watson_df, params={"rename_type": rename_type})
prefix = "NOT-EXISTING-PREFIX"
ret_list = list(func_to_test(ctx, args, prefix))
assert not ret_list


@pytest.mark.datafiles(AUTOCOMPLETION_FRAMES_PATH)
@pytest.mark.parametrize(
"func_to_test, prefix, n_expected_vals, rename_type, args",
[
(get_frames, "f4f7", N_FRAME_IDS_FOR_PREFIX, None, []),
(
get_project_or_task_completion,
"+tag",
N_TASKS,
None,
["project1", "+tag3"],
),
(get_project_or_task_completion, "+tag", N_TASKS, None, ["project1"]),
(
get_project_or_task_completion,
"project3",
N_VARIATIONS_OF_PROJECT3,
None,
[],
),
(get_projects, "project3", N_VARIATIONS_OF_PROJECT3, None, []),
(get_rename_name, "project3", N_VARIATIONS_OF_PROJECT3, "project", []),
(get_rename_name, "tag", N_TASKS, "tag", []),
(get_rename_types, "ta", 1, None, []),
(get_tags, "tag", N_TASKS, None, []),
],
)
def test_completion_of_existing_prefix(
watson_df, func_to_test, prefix, n_expected_vals, rename_type, args
):
ctx = ClickContext(obj=watson_df, params={"rename_type": rename_type})
ret_set = set(func_to_test(ctx, args, prefix))
assert len(ret_set) == n_expected_vals
assert all(cur_elem.startswith(prefix) for cur_elem in ret_set)


@pytest.mark.datafiles(AUTOCOMPLETION_FRAMES_PATH)
@pytest.mark.parametrize(
"func_to_test, prefix, expected_vals",
[
(get_rename_types, "", ["project", "tag"]),
(get_rename_types, "t", ["tag"]),
(get_rename_types, "p", ["project"]),
],
)
def test_for_known_completion_values(func_to_test, prefix, expected_vals):
ret_list = list(func_to_test(None, [], prefix))
assert ret_list == expected_vals
8 changes: 1 addition & 7 deletions tests/test_watson.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,15 @@

import arrow
from click import get_app_dir
import py
import pytest
import requests

from watson import Watson, WatsonError
from watson.watson import ConfigParser, ConfigurationError
from watson.utils import PY2

from . import mock_read
from . import mock_read, TEST_FIXTURE_DIR

TEST_FIXTURE_DIR = py.path.local(
os.path.dirname(
os.path.realpath(__file__)
)
) / 'resources'

if not PY2:
builtins = 'builtins'
Expand Down
Loading

0 comments on commit c3a1a4d

Please sign in to comment.