Skip to content

Commit fa0d0a0

Browse files
Set up shell command-line tab-completion for jupyter and subcommands (#337)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 354b6ea commit fa0d0a0

File tree

5 files changed

+67
-4
lines changed

5 files changed

+67
-4
lines changed

examples/jupyter-completion.bash

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
# load with: . jupyter-completion.bash
2+
#
3+
# NOTE: with traitlets>=5.8, jupyter and its subcommands now directly support
4+
# shell command-line tab-completion using argcomplete, which has more complete
5+
# support than this script. Simply install argcomplete and activate global
6+
# completion by following the relevant instructions in:
7+
# https://kislyuk.github.io/argcomplete/#activating-global-completion
28

39
if [[ -n ${ZSH_VERSION-} ]]; then
410
autoload -Uz bashcompinit && bashcompinit

jupyter_core/command.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# PYTHON_ARGCOMPLETE_OK
12
"""The root `jupyter` command.
23
34
This does nothing other than dispatch to subcommands or output path info.
@@ -37,6 +38,15 @@ def epilog(self, x):
3738
"""Ignore epilog set in Parser.__init__"""
3839
pass
3940

41+
def argcomplete(self):
42+
"""Trigger auto-completion, if enabled"""
43+
try:
44+
import argcomplete # type: ignore[import]
45+
46+
argcomplete.autocomplete(self)
47+
except ImportError:
48+
pass
49+
4050

4151
def jupyter_parser() -> JupyterParser:
4252
"""Create a jupyter parser object."""
@@ -48,7 +58,11 @@ def jupyter_parser() -> JupyterParser:
4858
group.add_argument(
4959
"--version", action="store_true", help="show the versions of core jupyter packages and exit"
5060
)
51-
group.add_argument("subcommand", type=str, nargs="?", help="the subcommand to launch")
61+
subcommand_action = group.add_argument(
62+
"subcommand", type=str, nargs="?", help="the subcommand to launch"
63+
)
64+
# For argcomplete, supply all known subcommands
65+
subcommand_action.completer = lambda *args, **kwargs: list_subcommands() # type: ignore[attr-defined]
5266

5367
group.add_argument("--config-dir", action="store_true", help="show Jupyter config dir")
5468
group.add_argument("--data-dir", action="store_true", help="show Jupyter data dir")
@@ -173,13 +187,49 @@ def _path_with_self():
173187
return path_list
174188

175189

190+
def _evaluate_argcomplete(parser: JupyterParser) -> List[str]:
191+
"""If argcomplete is enabled, trigger autocomplete or return current words
192+
193+
If the first word looks like a subcommand, return the current command
194+
that is attempting to be completed so that the subcommand can evaluate it;
195+
otherwise auto-complete using the main parser.
196+
"""
197+
try:
198+
# traitlets >= 5.8 provides some argcomplete support,
199+
# use helper methods to jump to argcomplete
200+
from traitlets.config.argcomplete_config import (
201+
get_argcomplete_cwords,
202+
increment_argcomplete_index,
203+
)
204+
205+
cwords = get_argcomplete_cwords()
206+
if cwords and len(cwords) > 1 and not cwords[1].startswith("-"):
207+
# If first completion word looks like a subcommand,
208+
# increment word from which to start handling arguments
209+
increment_argcomplete_index()
210+
return cwords
211+
else:
212+
# Otherwise no subcommand, directly autocomplete and exit
213+
parser.argcomplete()
214+
except ImportError:
215+
# traitlets >= 5.8 not available, just try to complete this without
216+
# worrying about subcommands
217+
parser.argcomplete()
218+
raise AssertionError("Control flow should not reach end of autocomplete()")
219+
220+
176221
def main() -> None:
177222
"""The command entry point."""
178223
parser = jupyter_parser()
179-
if len(sys.argv) > 1 and not sys.argv[1].startswith("-"):
224+
argv = sys.argv
225+
subcommand = None
226+
if "_ARGCOMPLETE" in os.environ:
227+
argv = _evaluate_argcomplete(parser)
228+
subcommand = argv[1]
229+
elif len(argv) > 1 and not argv[1].startswith("-"):
180230
# Don't parse if a subcommand is given
181231
# Avoids argparse gobbling up args passed to subcommand, such as `-h`.
182-
subcommand = sys.argv[1]
232+
subcommand = argv[1]
183233
else:
184234
args, opts = parser.parse_known_args()
185235
subcommand = args.subcommand
@@ -343,7 +393,7 @@ def main() -> None:
343393
sys.exit(str(e))
344394

345395
try:
346-
_execvp(command, [command] + sys.argv[2:])
396+
_execvp(command, [command] + argv[2:])
347397
except OSError as e:
348398
sys.exit(f"Error executing Jupyter command {subcommand!r}: {e}")
349399

jupyter_core/migrate.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# PYTHON_ARGCOMPLETE_OK
12
"""Migrating IPython < 4.0 to Jupyter
23
34
This *copies* configuration and resources to their new locations in Jupyter

jupyter_core/troubleshoot.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ def main() -> None:
5151
"""
5252
# pylint: disable=superfluous-parens
5353
# args = get_args()
54+
if "_ARGCOMPLETE" in os.environ:
55+
# No arguments to complete, the script can be slow to run to completion,
56+
# so in case someone tries to complete jupyter troubleshoot just exit early
57+
return
58+
5459
environment_data = get_data()
5560

5661
print("$PATH:")

scripts/jupyter-migrate

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env python
2+
# PYTHON_ARGCOMPLETE_OK
23
"""Migrate Jupyter config from IPython < 4.0"""
34

45
from jupyter_core.migrate import main

0 commit comments

Comments
 (0)