Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ Added
``dict[str, SomeBaseClass]``, now have a ``--*.help`` option, and subclasses
in any container are now included in the known subclasses shown in the help
(`#960 <https://github.com/mauvilsa/jsonargparse/pull/960>`__).
- New ``jsonschema`` completion type, i.e. ``--print_completion=jsonschema`` and
``parser.get_completion_script("jsonschema")``, which generates a JSON Schema
(draft 2020-12) that describes the config files accepted by the parser,
including descriptions from docstrings, defaults, required keys, type
restrictions, the types that the plain argparse actions give and one entry per
known subclass of subclass types. Configs can point to a schema with a
``$schema`` key, which is ignored when parsing. This feature is
experimental, so the details of the generated schema might change in non-major
releases (`#961 <https://github.com/mauvilsa/jsonargparse/pull/961>`__).

Fixed
^^^^^
Expand Down
203 changes: 158 additions & 45 deletions DOCUMENTATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3449,67 +3449,182 @@ function to provide an instance of the parser to :class:`.ActionParser`.


.. _tab-completion:
.. _completion-scripts:

Tab completion
==============
Completion scripts
==================

Tab completion is available for jsonargparse parsers by using either the `shtab
<https://pypi.org/project/shtab/>`__ package or the `argcomplete
<https://pypi.org/project/argcomplete/>`__ package.
From a parser, jsonargparse can generate artifacts that describe what the parser
accepts, so that other tools can validate and complete configs and command
lines. The supported completion types are:

shtab
-----
- ``jsonschema``: a JSON Schema that describes the config files that the parser
accepts. Always available.
- ``shtab-*``: a completion script for a given shell, e.g. ``shtab-bash``.
Available when the `shtab <https://pypi.org/project/shtab/>`__ package is
installed.

Both are generated with the :meth:`.ArgumentParser.get_completion_script`
method, or from the command line, see :ref:`print-completion-argument`.

Covered further down is completion at runtime in the shell, which jsonargparse
supports through the `argcomplete <https://pypi.org/project/argcomplete/>`__
package, see :ref:`argcomplete`. It does not involve any generated artifact.

For ``shtab`` to work, there is no need to set ``complete``/``choices`` to the
parser actions, and no need to call `shtab.add_argument_to
<https://docs.iterative.ai/shtab/ref/#add_argument_to>`__. This is done
automatically by :meth:`parse_args <.ArgumentParser.parse_args>`. The only
requirement is to install shtab either directly or by installing jsonargparse
with the ``shtab`` extra as explained in section :ref:`installation`.

There are two ways to generate shell completion scripts when ``shtab`` is
installed: via the :meth:`.ArgumentParser.get_completion_script` method or by
enabling a command-line argument.
.. _print-completion-argument:

Programmatic generation
^^^^^^^^^^^^^^^^^^^^^^^
The --print_completion argument
-------------------------------

The :meth:`.ArgumentParser.get_completion_script` method can be used to
generate completion scripts programmatically. The method accepts a
``completion_type`` parameter that specifies the shell. For shtab, use
``shtab-`` followed by the shell name (e.g., ``shtab-bash``, ``shtab-zsh``).
To enable generation of completion scripts via the command line, use
:func:`.set_parsing_settings` with ``add_print_completion_argument=True``. This
adds a ``--print_completion`` argument to top-level parsers (not subparsers),
which accepts the completion types listed above.

.. testcode::

from jsonargparse import ArgumentParser
from jsonargparse import set_parsing_settings

set_parsing_settings(add_print_completion_argument=True)

Without changing python code, it is also possible to add the
``--print_completion`` argument by setting the environment variable
``JSONARGPARSE_ADD_PRINT_COMPLETION_ARGUMENT=true``.


jsonschema
----------

The ``jsonschema`` completion type gives a `JSON Schema
<https://json-schema.org/>`__ (draft 2020-12) that describes the config files
accepted by the parser.

.. testcode::

parser = ArgumentParser(prog="example")
parser.add_argument("--bool", type=bool)

script = parser.get_completion_script("shtab-bash", preambles=[])
# script now contains the bash completion script
schema = parser.get_completion_script("jsonschema")
# schema now contains the JSON schema

.. warning::
The equivalent from the command line is:

After calling :meth:`.get_completion_script`, the parser instance is
invalidated and cannot be used for parsing arguments. Create a new parser
instance if you need to parse arguments afterward.
.. code-block:: bash

Command-line argument
^^^^^^^^^^^^^^^^^^^^^
$ example.py --print_completion=jsonschema > schema.json

To enable generation of completion scripts via a command-line argument, use
:func:`.set_parsing_settings` with ``add_print_completion_argument=True``. This
adds a ``--print_completion`` argument to top-level parsers (not subparsers).
This schema is useful as a machine-readable interface for tools. For example:

- IDE/editor assistance (autocompletion, hints, and inline validation).
- Config contract checks in CI pipelines.
- Generating documentation from parser structure.

To get validation and autocompletion for a config file in an editor such as
`Visual Studio Code
<https://code.visualstudio.com/docs/languages/json#_json-schemas-and-settings>`__,
the config can point to the generated schema with a ``$schema`` key:

.. code-block:: json

{
"$schema": "./schema.json",
"bool": true
}

The key is accepted in any config that a parser loads, :ref:`sub-config-files`
included, and it is removed before parsing, so it never becomes part of the
parsed namespace. Accordingly, every object in the schema that describes a
config accepts the key.

The schema is derived from the same information that the ``--help`` output is
based on, so it includes:

- The structure of nested keys, i.e. argument groups and subclasses-disabled
types become objects, and which of their keys are required.
- The accepted types, including unions, literals, enums, containers and the
restrictions of types such as :class:`.PositiveInt` and :class:`.Email`. For
the plain argparse actions, which have no type hint, this is what the action
gives, e.g. a boolean for ``store_true``, an integer for ``count``, the
possible values for ``store_const`` and an array for ``append``.
- The defaults of the arguments, except for the required ones, the ones whose
default is ``argparse.SUPPRESS``, since not giving those leaves no key, and the
unset ones, see :ref:`unset-values`. Without ``unset_sentinel``, a ``None``
default is unset, so ``null`` is never described as a default. With it, an
explicit ``default=None`` is described, as long as the type accepts ``null``.
- Descriptions taken from the docstrings of the classes and functions that the
arguments come from, or from the ``help`` given to ``add_argument``.
- For subclass types, one entry per known subclass, each with a ``class_path``
fixed to that subclass and an ``init_args`` object describing the accepted
init parameters of that specific class.
- For parsers with subcommands, one object per subcommand and a ``subcommand``
key. This key is optional, since a config that has a single subcommand block
implies it, and when a config has several blocks the subcommand can be given
as a command line argument.

Subclasses and types that are used in more than one place are added once to
``$defs`` and referenced with ``$ref``, which also makes recursive types work.

The schema is intended to accept what the parser accepts, though for subclass
types it is stricter: a string is accepted, since it can be a class path or a
path to a sub-config file, but an object is only accepted for the known
subclasses, i.e. one with ``class_path``, ``init_args`` (mandatory only for the
subclasses that have a required init parameter) and ``dict_kwargs``. An object
that accepts any ``class_path`` would keep tools from suggesting the known
subclasses and from pointing out a class path that has a typo or is not the
accepted import path, in which case the ``init_args`` would go undescribed. Only
when a type has no known subclass is any ``class_path`` accepted, without
describing its ``init_args``.

A union with a subtype that accepts anything, i.e. ``Any`` or an unvalidated
type, is kept as ``{"anyOf": [..., {}]}`` instead of the equivalent ``{}``, so
that tools still have the other subschemas to describe and complete against. The
exception is when another subtype constrains the keys of an object, e.g. a
subclass, dataclass or typed dict. Then the subschemas that accept any object,
i.e. from ``Any``, ``dict`` and unvalidated types, are excluded, making the
schema stricter than the parser, but in exchange mistakes in the keys are
pointed out instead of going unnoticed.

.. note::

The subclasses of a type that the schema includes are the ones known to
python at the time the schema is generated, i.e. only those whose modules
happen to have been imported.

.. note::

The ``jsonschema`` completion type is experimental. The details of the
generated schema might change in non-major releases.


shtab
-----

The ``shtab-*`` completion types give a shell completion script, using
``shtab-`` followed by the shell name, e.g. ``shtab-bash`` or ``shtab-zsh``.

For ``shtab`` to work, there is no need to set ``complete``/``choices`` to the
parser actions, and no need to call `shtab.add_argument_to
<https://docs.iterative.ai/shtab/ref/#add_argument_to>`__. The only
requirement is to install shtab either directly or by installing jsonargparse
with the ``shtab`` extra as explained in section :ref:`installation`.

.. testcode::

from jsonargparse import set_parsing_settings
parser = ArgumentParser(prog="example")
parser.add_argument("--bool", type=bool)

set_parsing_settings(add_print_completion_argument=True)
script = parser.get_completion_script("shtab-bash", preambles=[])
# script now contains the bash completion script

.. warning::

After calling :meth:`.get_completion_script` for an ``shtab-*`` completion
type, the parser instance is invalidated and cannot be used for parsing
arguments.

With this setting enabled, completion scripts can be generated from the command
line. For example, in Linux to enable bash completions for all users, as root:
From the command line, for example in Linux to enable bash completions for all
users, as root:

.. code-block:: bash

Expand All @@ -3522,10 +3637,6 @@ them:

$ eval "$(example.py --print_completion=shtab-bash)"

Without changing python code, it is also possible to add the ``--print_completion``
argument by setting the environment variable
``JSONARGPARSE_ADD_PRINT_COMPLETION_ARGUMENT=true``.

Completion behavior
^^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -3586,23 +3697,25 @@ completed, as well as the values that they accept, e.g.:
Expected type: bool; 2/2 matched choices
true false

.. _argcomplete:

argcomplete
-----------

For ``argcomplete`` to work, there is no need to implement completer functions
or to call `argcomplete.autocomplete
<https://kislyuk.github.io/argcomplete/#argcomplete.autocomplete>`__ since this
is done automatically by :meth:`parse_args <.ArgumentParser.parse_args>`. The
only requirement to enable tab completion is to install argcomplete either
only requirement to enable shell completion is to install argcomplete either
directly or by installing jsonargparse with the ``argcomplete`` extra as
explained in section :ref:`installation`.

The tab completion can be enabled `globally
The shell completion can be enabled `globally
<https://kislyuk.github.io/argcomplete/#global-completion>`__ for all
argcomplete compatible tools or for each `individual
<https://kislyuk.github.io/argcomplete/#synopsis>`__ tool.

Using the same ``bool`` example as shown above, activate tab completion and use
Using the same ``bool`` example as shown above, activate completion and use
it as follows:

.. code-block:: bash
Expand Down
9 changes: 7 additions & 2 deletions jsonargparse/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@ class ImportDenied(ImportError, ValueError):
)


# key that configs may have to point to a JSON Schema, only for editor support, so ignored when parsing
config_schema_key = "$schema"
Comment thread
mauvilsa marked this conversation as resolved.
Dismissed


parsing_settings: dict = {
"validate_defaults": False,
"validate_subclass_spec_in_any": False,
Expand Down Expand Up @@ -410,8 +414,9 @@ class when a value for a type that accepts any value, i.e. ``Any``,
positionals are applied to optionals in the order that they were
added to the parser. By default, this is ``False``.
add_print_completion_argument: If ``True``, top-level parsers
automatically include ``--print_completion`` argument when
``shtab`` is installed.
automatically include a ``--print_completion`` argument. Its
accepted values are ``jsonschema`` and, when ``shtab`` is installed,
one ``shtab-*`` value per supported shell.
stubs_resolver_allow_py_files: Whether the stubs resolver should search
in ``.py`` files in addition to ``.pyi`` files.
omegaconf_absolute_to_relative_paths: If ``True``, when loading configs
Expand Down
20 changes: 15 additions & 5 deletions jsonargparse/_completions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import argparse
import json
import locale
import os
import re
Expand Down Expand Up @@ -43,7 +44,7 @@ def handle_completions(parser):


def add_print_completion_argument(parser):
if getattr(parser, "parent_parser", None) or not find_spec("shtab"):
if getattr(parser, "parent_parser", None):
return
print_completion_argument = get_parsing_setting("add_print_completion_argument")
if not print_completion_argument and "--print_shtab" not in parser._option_string_actions:
Expand Down Expand Up @@ -128,14 +129,17 @@ def __init__(
default=argparse.SUPPRESS,
**kwargs,
):
import shtab
choices = ["jsonschema"]
if find_spec("shtab"):
import shtab

choices.extend(f"shtab-{shell}" for shell in shtab.SUPPORTED_SHELLS)
super().__init__(
option_strings=option_strings,
dest=dest,
default=default,
choices=[f"shtab-{shell}" for shell in shtab.SUPPORTED_SHELLS],
help="Print shell completion script.",
choices=choices,
help="Print completion script.",
)

def __call__(self, parser, namespace, completion_type, option_string=None):
Expand All @@ -144,11 +148,17 @@ def __call__(self, parser, namespace, completion_type, option_string=None):


def get_completion_script(parser, completion_type: str, **kwargs) -> str:
if completion_type == "jsonschema":
from ._completions_jsonschema import config_jsonschema

return json.dumps(config_jsonschema(parser), indent=2) # doesn't modify the parser
if not completion_type.startswith("shtab-"):
raise ValueError(f"Unsupported completion_type: {completion_type}.")
if not find_spec("shtab"):
raise ValueError(f"shtab package is required for completion type '{completion_type}'.")
return get_shtab_script(parser, completion_type[len("shtab-") :], **kwargs)
script = get_shtab_script(parser, completion_type[len("shtab-") :], **kwargs)
parser._invalidate_by_completion_script()
return script


def get_shtab_script(parser, shell: str, preambles: list[str] | None = None) -> str:
Expand Down
Loading