add module names completion for install (#103)

* add module names completion for install

* sort completion suggestions

* add instructions in the readme

* switch to click 8

* update setup.py to click 8

* fix from rebase (missing bundle list)

* try not to update bundles for shell completion
This commit is contained in:
Neradoc 2021-06-27 23:02:30 +02:00 committed by GitHub
parent 820dc0a7be
commit f797cdd08e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 112 additions and 6 deletions

View file

@ -211,6 +211,63 @@ The ``--version`` flag will tell you the current version of the
That's it!
Library Name Autocomplete
-------------------------
When enabled, circup will autocomplete library names, simliar to other command line tools.
For example:
``circup install n`` + tab -``circup install neopixel`` (+tab: offers ``neopixel`` and ``neopixel_spi`` completions)
``circup install a`` + tab -``circup install adafruit\_`` + m a g + tab -``circup install adafruit_magtag``
How to Activate Library Name Autocomplete
-----------------------------------------
In order to activate shell completion, you need to inform your shell that completion is available for your script. Any Click application automatically provides support for that.
For Bash, add this to ~/.bashrc::
eval "$(_CIRCUP_COMPLETE=bash_source circup)"
For Zsh, add this to ~/.zshrc::
eval "$(_CIRCUP_COMPLETE=zsh_source circup)"
For Fish, add this to ~/.config/fish/completions/foo-bar.fish::
eval (env _CIRCUP_COMPLETE=fish_source circup)
Open a new shell to enable completion. Or run the eval command directly in your current shell to enable it temporarily.
### Activation Script
The above eval examples will invoke your application every time a shell is started. This may slow down shell startup time significantly.
Alternatively, export the generated completion code as a static script to be executed. You can ship this file with your builds; tools like Git do this. At least Zsh will also cache the results of completion files, but not eval scripts.
For Bash::
_CIRCUP_COMPLETE=bash_source circup circup-complete.sh
For Zsh::
_CIRCUP_COMPLETE=zsh_source circup circup-complete.sh
For Fish::
_CIRCUP_COMPLETE=fish_source circup circup-complete.sh
In .bashrc or .zshrc, source the script instead of the eval command::
. /path/to/circup-complete.sh
For Fish, add the file to the completions directory::
_CIRCUP_COMPLETE=fish_source circup ~/.config/fish/completions/circup-complete.fish
.. note::
If you find a bug, or you want to suggest an enhancement or new feature

View file

@ -385,10 +385,25 @@ def clean_library_name(assumed_library_name):
return assumed_library_name
def completion_for_install(ctx, param, incomplete):
"""
Returns the list of available modules for the command line tab-completion
with the ``circup install`` command.
"""
# pylint: disable=unused-argument
available_modules = get_bundle_versions(get_bundles_list(), avoid_download=True)
module_names = {m.replace(".py", "") for m in available_modules}
if incomplete:
module_names = [name for name in module_names if name.startswith(incomplete)]
return sorted(module_names)
def ensure_latest_bundle(bundle):
"""
Ensure that there's a copy of the latest library bundle available so circup
can check the metadata contained therein.
:param Bundle bundle: the target Bundle object.
"""
logger.info("Checking library updates for %s.", bundle.key)
tag = bundle.latest_tag
@ -639,19 +654,21 @@ def get_bundle(bundle, tag):
click.echo("\nOK\n")
def get_bundle_versions(bundles_list):
def get_bundle_versions(bundles_list, avoid_download=False):
"""
Returns a dictionary of metadata from modules in the latest known release
of the library bundle. Uses the Python version (rather than the compiled
version) of the library modules.
:param Bundle bundles_list: List of supported bundles as Bundle objects.
:param bool avoid_download: if True, download the bundle only if missing.
:return: A dictionary of metadata about the modules available in the
library bundle.
"""
all_the_modules = dict()
for bundle in bundles_list:
ensure_latest_bundle(bundle)
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
ensure_latest_bundle(bundle)
path = bundle.lib_dir("py")
path_modules = get_modules(path)
for name, module in path_modules.items():
@ -1102,7 +1119,9 @@ def list(ctx): # pragma: no cover
@main.command()
@click.argument("modules", required=False, nargs=-1)
@click.argument(
"modules", required=False, nargs=-1, shell_complete=completion_for_install
)
@click.option("--py", is_flag=True)
@click.option("-r", "--requirement")
@click.pass_context

View file

@ -10,7 +10,7 @@ black==19.3b0
bleach==3.3.0
certifi==2019.6.16
chardet==3.0.4
Click>=7.0
Click>=8.0
coverage==4.5.4
docutils==0.15.2
idna==2.8

View file

@ -24,7 +24,7 @@ with open(path.join(here, "README.rst"), encoding="utf-8") as f:
install_requires = [
"semver~=2.13",
"Click>=7.0",
"Click>=8.0",
"appdirs>=1.4.3",
"requests>=2.22.0",
]

View file

@ -564,20 +564,50 @@ def test_find_modules_goes_bang():
def test_get_bundle_versions():
"""
Ensure get_modules is called with the path for the library bundle.
Ensure ensure_latest_bundle is called even if lib_dir exists.
"""
with mock.patch("circup.ensure_latest_bundle"), mock.patch(
with mock.patch("circup.ensure_latest_bundle") as mock_elb, mock.patch(
"circup.get_modules", return_value={"ok": {"name": "ok"}}
) as mock_gm, mock.patch("circup.CPY_VERSION", "4.1.2"), mock.patch(
"circup.Bundle.lib_dir", return_value="foo/bar/lib"
), mock.patch(
"circup.os.path.isdir", return_value=True
):
bundle = circup.Bundle(TEST_BUNDLE_NAME)
bundles_list = [bundle]
assert circup.get_bundle_versions(bundles_list) == {
"ok": {"name": "ok", "bundle": bundle}
}
mock_elb.assert_called_once_with(bundle)
mock_gm.assert_called_once_with("foo/bar/lib")
def test_get_bundle_versions_avoid_download():
"""
When avoid_download is True and lib_dir exists, don't ensure_latest_bundle.
Testing both cases: lib_dir exists and lib_dir doesn't exists.
"""
with mock.patch("circup.ensure_latest_bundle") as mock_elb, mock.patch(
"circup.get_modules", return_value={"ok": {"name": "ok"}}
) as mock_gm, mock.patch("circup.CPY_VERSION", "4.1.2"), mock.patch(
"circup.Bundle.lib_dir", return_value="foo/bar/lib"
):
bundle = circup.Bundle(TEST_BUNDLE_NAME)
bundles_list = [bundle]
with mock.patch("circup.os.path.isdir", return_value=True):
assert circup.get_bundle_versions(bundles_list, avoid_download=True) == {
"ok": {"name": "ok", "bundle": bundle}
}
assert mock_elb.call_count == 0
mock_gm.assert_called_once_with("foo/bar/lib")
with mock.patch("circup.os.path.isdir", return_value=False):
assert circup.get_bundle_versions(bundles_list, avoid_download=True) == {
"ok": {"name": "ok", "bundle": bundle}
}
mock_elb.assert_called_once_with(bundle)
mock_gm.assert_called_with("foo/bar/lib")
def test_get_circuitpython_version():
"""
Given valid content of a boot_out.txt file on a connected device, return