Skip to content

Commit d950f3f

Browse files
authored
Support functions for callable protocols and typing aliases, plus a talks and articles page (#963)
1 parent 72c4885 commit d950f3f

14 files changed

Lines changed: 562 additions & 142 deletions

CHANGELOG.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ Added
3838
``$schema`` key, which is ignored when parsing. This feature is
3939
experimental, so the details of the generated schema might change in non-major
4040
releases (`#961 <https://github.com/mauvilsa/jsonargparse/pull/961>`__).
41+
- A protocol whose single method is ``__call__`` is now also implemented by a
42+
function with a compatible signature, so the import path of a function is
43+
accepted as value, see :ref:`type-hints` (`#963
44+
<https://github.com/mauvilsa/jsonargparse/pull/963>`__).
4145

4246
Fixed
4347
^^^^^
@@ -59,6 +63,21 @@ Fixed
5963
type is nested in a container or optional, e.g. ``builtins.NoneType`` for
6064
``Optional[list[SomeBaseClass]]`` (`#960
6165
<https://github.com/mauvilsa/jsonargparse/pull/960>`__).
66+
- Functions without a return annotation at runtime, e.g. C-implemented ones like
67+
``time.localtime``, were rejected for ``Callable`` types with a class return
68+
type, even when a stub file gives the return type. Now with the stubs resolver
69+
the return type from the ``.pyi`` is used (`#963
70+
<https://github.com/mauvilsa/jsonargparse/pull/963>`__).
71+
- Unsubscripted ``typing`` sequence aliases, i.e. ``List``, ``Sequence``,
72+
``MutableSequence``, ``Iterable``, ``Collection``, ``Container``,
73+
``Reversible`` and ``Deque``, raised ``AttributeError: __args__``, which made
74+
classes that have one in their signature, e.g.
75+
``torch.utils.data.DataLoader``, impossible to add (`#963
76+
<https://github.com/mauvilsa/jsonargparse/pull/963>`__).
77+
- ``typing.Hashable`` and ``typing.Sized`` were not supported, while the
78+
``collections.abc`` spelling of the same types was. A bare one didn't validate
79+
and a composed one, e.g. ``Optional[Hashable]``, raised ``Unsupported type
80+
hint`` (`#963 <https://github.com/mauvilsa/jsonargparse/pull/963>`__).
6281

6382
Deprecated
6483
^^^^^^^^^^

CONTRIBUTING.rst

Lines changed: 73 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,58 @@
1+
.. _contributing:
2+
13
Contributing
24
============
35

4-
Contributions to jsonargparse are very welcome. There are multiple ways for
5-
people to help and contribute, among them:
6+
Contributions to jsonargparse are very welcome. There are many ways to help,
7+
among them:
68

79
- Star ⭐ the GitHub project `<https://github.com/mauvilsa/jsonargparse/>`__.
810
- `Sponsor 🩷 <https://github.com/sponsors/mauvilsa>`__ its maintenance and
911
development.
1012
- Spread the word in your community about the features you like from
1113
jsonargparse.
12-
- Help others to learn how to use jsonargparse by creating tutorials, such as
13-
blog posts and videos.
14+
- Help others learn how to use jsonargparse by creating tutorials, such as blog
15+
posts and videos. If you do, let us know so that it can be added to
16+
:ref:`talks-and-articles`.
1417
- Become active in existing GitHub issues and pull requests.
15-
- Create `issues <https://github.com/mauvilsa/jsonargparse/issues>`__ for
16-
reporting bugs and proposing improvements.
18+
- Create `issues <https://github.com/mauvilsa/jsonargparse/issues>`__ to report
19+
bugs and propose improvements.
1720
- Create `pull requests <https://github.com/mauvilsa/jsonargparse/pulls>`__ with
1821
documentation improvements, bug fixes or new features.
1922

2023
.. note::
2124

22-
While creating an issue before submitting a pull request is not mandatory,
23-
it might be helpful. Issues allow for discussion and feedback before
24-
significant development effort is invested. However, in some cases, code
25-
changes can better illustrate a proposal, making it more effective to submit
26-
a pull request directly. In such cases please avoid opening a largely
27-
redundant issue.
25+
Creating an issue before submitting a pull request is not mandatory, but it
26+
can be helpful, since it allows for discussion and feedback before
27+
significant effort is invested. In some cases, though, code changes
28+
illustrate a proposal better, so submitting a pull request directly is more
29+
effective. In such cases please avoid opening a largely redundant issue.
2830

2931
Development environment
3032
-----------------------
3133

32-
If you intend to work with the source code, note that this project does not
33-
include any ``requirements.txt`` file. This is by intention. To make it very
34-
clear what are the requirements for different use cases, all the requirements of
35-
the project are stored in the file ``pyproject.toml``. The basic runtime
36-
requirements are defined in ``dependencies``. Requirements for optional features
37-
are stored in ``[project.optional-dependencies]``. Also in the same section
38-
there are requirements for testing, development and documentation building:
39-
``test``, ``dev`` and ``doc``.
34+
All requirements of the project are defined in ``pyproject.toml``. The basic
35+
runtime requirements are in ``dependencies``. Requirements for optional
36+
features, as well as for testing, development and documentation building
37+
(``test``, ``dev`` and ``doc``), are in ``[project.optional-dependencies]``.
4038

41-
The recommended way to work with the source code is the following. First clone
42-
the repository, then create a virtual environment, activate it and finally
43-
install the development requirements. More precisely the steps are:
39+
The recommended way to work with the source code is to clone the repository,
40+
create a virtual environment, activate it, and install the development
41+
requirements:
4442

4543
.. code-block:: bash
4644
4745
git clone https://github.com/mauvilsa/jsonargparse.git
4846
cd jsonargparse
4947
python -m venv venv
5048
. venv/bin/activate
51-
52-
The crucial step is installing the requirements which would be done by running:
53-
54-
.. code-block:: bash
55-
5649
pip install -e ".[dev,all]"
5750
5851
pre-commit
5952
----------
6053

61-
Please also install the `pre-commit <https://pre-commit.com/>`__ git hooks so
62-
that unit tests and code checks are automatically run locally. This is done as
63-
follows:
54+
Please also install the `pre-commit <https://pre-commit.com/>`__ git hooks, so
55+
that unit tests and code checks run automatically on your machine:
6456

6557
.. code-block:: bash
6658
@@ -69,19 +61,16 @@ follows:
6961
.. note::
7062

7163
``.pre-commit-config.yaml`` is configured to run the hooks using Python
72-
3.12. Ensure you have Python 3.12 installed and available in your
73-
environment for ``pre-commit`` to function correctly. For development, other
74-
Python versions will work, but for convenience, Python 3.12 is recommended.
75-
76-
The ``pre-push`` stage runs several hooks, including tests, doctests, mypy, and
77-
coverage. These hooks are designed to inform developers of issues that must be
78-
resolved before a pull request can be merged. Note that these hooks may take
79-
some time to complete. If you wish to push without running these hooks, use the
80-
command ``git push --no-verify``.
64+
3.12, so make sure that this version is installed and available. Other
65+
Python versions work for development, but 3.12 is recommended for
66+
convenience.
8167

82-
Formatting of the code is done automatically by pre-commit. If some pre-commit
83-
hooks fail and you decide to skip them, formatting will be automatically applied
84-
by a GitHub action in pull requests.
68+
The ``pre-push`` stage runs several hooks, including tests, doctests, mypy and
69+
coverage. They inform developers of issues that must be resolved before a pull
70+
request can be merged, and can take some time to complete. To push without
71+
running them, use ``git push --no-verify``. Formatting of the code is applied
72+
automatically by pre-commit. Even when pushing with ``--no-verify``, please make
73+
sure that the formatting has been applied.
8574

8675
Documentation
8776
-------------
@@ -92,8 +81,7 @@ To build the documentation run:
9281
9382
sphinx-build sphinx sphinx/_build sphinx/*.rst
9483
95-
To view the built documentation, open the file ``sphinx/_build/index.html`` in a
96-
browser.
84+
Then open the file ``sphinx/_build/index.html`` in a browser.
9785

9886
Code conventions
9987
----------------
@@ -103,7 +91,7 @@ Code conventions
10391
Most module filenames start with ``_``, meaning they are private implementation
10492
details. For objects within modules, the ``_`` prefix indicates the object is
10593
only used within that same module. An object without a ``_`` prefix may be
106-
imported by other modules but that does not make it public — it is simply
94+
imported by other modules, but that does not make it public — it is simply
10795
internal to the package. The only truly public objects are those listed in
10896
``jsonargparse.__all__`` and ``jsonargparse.typing.__all__``.
10997

@@ -116,9 +104,8 @@ style
116104
Tests
117105
-----
118106

119-
Running the unit tests can be done either using `pytest
120-
<https://docs.pytest.org/>`__ or `tox
121-
<https://tox.readthedocs.io/en/stable/>`__. Also pre-commit runs some additional
107+
The unit tests can be run with `pytest <https://docs.pytest.org/>`__ or `tox
108+
<https://tox.readthedocs.io/en/stable/>`__. Pre-commit runs some additional
122109
tests.
123110

124111
.. code-block:: bash
@@ -127,23 +114,23 @@ tests.
127114
pytest # Run tests using pytest on the python of the environment
128115
pre-commit run -a --hook-stage pre-push # Run pre-push git hooks (tests, doctests, mypy, coverage)
129116
130-
Tests can be run in any environment without the source code. Before v4.47.0, the
131-
tests were included in the main package. Since v4.47.0, they are provided in a
132-
separate package. Prefer installing the tests package with the same version as
133-
the main package. For example, for v4.47.0 run:
117+
The tests can also be run in any environment without the source code. Since
118+
v4.47.0 they are provided in a separate package, whereas before they were
119+
included in the main package. Prefer installing the tests package with the same
120+
version as the main package, for example:
134121

135122
.. code-block:: bash
136123
137124
pip install jsonargparse_tests==4.47.0
138125
python -m jsonargparse_tests
139126
140127
All contributed features and bug fixes must include tests. For bug fixes, ensure
141-
the test fails without the code fix. Almost always tests should exercise only
142-
the public API. Testing internal functions directly is rarely justified and
128+
that the test fails without the code fix. Tests should almost always exercise
129+
only the public API; testing internal functions directly is rarely justified and
143130
should be avoided. For tests involving signatures, define the classes and
144-
functions used at the global module scope. Jsonargparse is not intended to
145-
support dynamically defined classes and functions, so there is no value in
146-
testing such cases.
131+
functions at the global module scope. Jsonargparse is not intended to support
132+
dynamically defined classes and functions, so there is no value in testing such
133+
cases.
147134

148135
For maintainable tests:
149136

@@ -153,31 +140,35 @@ For maintainable tests:
153140
multiple tests need the same files, parser configuration, or environment.
154141
- Avoid pushing trivial one-line setup into fixtures when it makes the test
155142
harder to read.
156-
- Keep setup separate from assertions so each test clearly shows the behavior
157-
being verified.
158-
- The pytest output must be clean. If a test causes log output, the logs must
159-
be captured and asserted using the ``logger`` fixture and ``capture_logs``
160-
context manager from ``conftest.py``.
143+
- Keep setup separate from assertions, so that each test clearly shows the
144+
behavior being verified.
145+
- Keep the pytest output clean. If a test causes log output, the logs must be
146+
captured and minimally asserted using the ``logger`` fixture and
147+
``capture_logs`` context manager from ``conftest.py``.
161148

162149

163150
Coverage
164151
--------
165152

166-
For a nice html test coverage report, run:
153+
Coverage is required to be 100% in ``jsonargparse/*`` files, with realistic
154+
tests and without unwarranted ``# pragma: no cover``. This ensures that all
155+
existing code is actually needed.
156+
157+
For a nice html coverage report, run:
167158

168159
.. code-block:: bash
169160
170161
pytest --cov --cov-report=html
171162
172163
Then open the file ``htmlcov/index.html`` in a browser.
173164

174-
To get a full coverage report, you need to install all supported python
175-
versions, and then:
165+
A full coverage report requires all supported Python versions to be installed,
166+
and then:
176167

177168
.. code-block:: bash
178169
179170
rm -fr jsonargparse_tests/.coverage jsonargparse_tests/htmlcov
180-
tox -- --cov=../jsonargparse --cov-append
171+
tox --parallel -- --cov=../jsonargparse --cov-append
181172
cd jsonargparse_tests
182173
coverage html
183174
@@ -186,19 +177,16 @@ Then open the file ``jsonargparse_tests/htmlcov/index.html`` in a browser.
186177
Pull requests
187178
-------------
188179

189-
When creating a pull request, it is recommended that you create a specific
190-
branch in your fork for the changes you want to contribute, instead of using the
191-
``main`` branch.
180+
For the changes you want to contribute, it is recommended to create a specific
181+
branch in your fork, instead of using the ``main`` branch.
192182

193-
The required tasks to do for a pull request, are listed in
194-
`PULL_REQUEST_TEMPLATE.md
183+
The tasks required for a pull request are listed in `PULL_REQUEST_TEMPLATE.md
195184
<https://github.com/mauvilsa/jsonargparse/blob/main/.github/PULL_REQUEST_TEMPLATE.md>`__.
196185

197-
One of the tasks is adding a changelog entry. For this, note that this project
198-
uses semantic versioning. Depending on whether the contribution is a bug fix or
199-
a new feature, the changelog entry would go in a patch or minor release. The
200-
changelog section for the next release does not have a definite date, for
201-
example:
186+
One of the tasks is adding a changelog entry. This project uses semantic
187+
versioning, so the entry goes in a patch release for a bug fix, or in a minor
188+
release for a new feature. The changelog section for the next release does not
189+
have a definite date, for example:
202190

203191
.. code-block::
204192
@@ -211,13 +199,14 @@ example:
211199
212200
If no such section exists, just add it with "(unreleased)" instead of a date.
213201
Have a look at previous releases to decide under which subsection the new entry
214-
should go. If you are unsure, ask in the pull request.
202+
should go. Entries must describe changes with respect to the previous release,
203+
not with respect to unreleased commits.
215204

216-
Please don't open pull requests with breaking changes unless this has been
205+
Please don't open pull requests with breaking changes, unless this has been
217206
discussed and agreed upon in an issue.
218207

219-
Contributions using coding agents are welcome. However, any agent-generated
220-
code must be fully understood by the submitter and must make sense and follow
221-
these contributing guidelines. Always ask the agent to read and follow these
222-
guidelines. Also ask to read ``.github/PULL_REQUEST_TEMPLATE.md`` so that the
223-
tasks before submitting are covered.
208+
Contributions using coding agents are welcome. However, any agent-generated code
209+
must be fully understood by the submitter, must make sense, and must follow
210+
these contributing guidelines. Always ask the agent to read and follow this
211+
document, and also ``.github/PULL_REQUEST_TEMPLATE.md``, so that the tasks
212+
required before submitting are covered.

DOCUMENTATION.rst

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -206,32 +206,6 @@ tool supports with their default values. Users can follow these steps:
206206
# Run the tool using the adapted config
207207
python example.py --config config.yaml
208208
209-
Comparison to Fire
210-
------------------
211-
212-
The :func:`.auto_cli` feature is similar to and inspired by `Fire
213-
<https://pypi.org/project/fire/>`__. However, there are fundamental differences.
214-
First, the purpose is not to allow calling any Python object from the command
215-
line. It is only intended for running functions and classes specifically written
216-
for this purpose. Second, the arguments are expected to have type hints, and the
217-
given values will be validated according to these. Third, the return values of
218-
the functions are not automatically printed. :func:`.auto_cli` returns the value
219-
and it is up to the developer to decide what to do with it.
220-
221-
222-
.. _tutorials:
223-
224-
Tutorials
225-
=========
226-
227-
- `"jsonargparse - Say goodbye to configuration hassles"
228-
<https://2022.pycon.de/program/XK73C3/>`__ by Marianne Stecklina at PyCon DE
229-
& PyData Berlin 2022
230-
231-
- Presentation video: https://youtu.be/2gDf2S0nHKg
232-
- GitHub repository: https://github.com/stecklin/pycon22-jsonargparse
233-
234-
235209
.. _parsers:
236210

237211
Parsers
@@ -587,7 +561,10 @@ Some notes about this support are:
587561
``Proto[int]``. Subscripting substitutes the type arguments in the protocol's
588562
methods, so ``Proto[int]`` and ``Proto[str]`` accept different
589563
implementations. A ``TypeVar`` that remains, in the protocol or in the
590-
implementation, matches any type, as static type checkers do.
564+
implementation, matches any type, as static type checkers do. A protocol whose
565+
single method is ``__call__`` is also implemented by a function with a
566+
compatible signature, in which case the value is the function itself, instead
567+
of a class to instantiate.
591568

592569
- ``dataclasses``, final classes, attrs' ``define``, pydantic's ``dataclass``
593570
and pydantic's ``BaseModel`` are supported even when nested. By default they
@@ -620,8 +597,10 @@ Some notes about this support are:
620597
subclass of the return type of the callable. For these cases running
621598
:meth:`instantiate <.ArgumentParser.instantiate>` will instantiate the class
622599
or provide a function that returns the instance of the class. For more details
623-
see :ref:`callable-type`. Currently the callable's argument and return types
624-
are not validated.
600+
see :ref:`callable-type`. A function given by import path must have a return
601+
annotation, or a return type in a stub file (see :ref:`stubs-resolver`), that
602+
is the callable's return type or a subclass of it. Argument types are not
603+
validated.
625604

626605
- ``types.ModuleType`` is supported by giving the dot import path of a module,
627606
and on ``instantiate`` is replaced by the imported module object.

README.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ a `substantial user base
2727
it serves as the framework behind pytorch-lightning's `LightningCLI
2828
<https://lightning.ai/docs/pytorch/stable/cli/lightning_cli.html>`__.
2929

30+
The documentation is a reference that describes each feature in isolation, which
31+
is not always the best way to learn. If you would rather see the *why* behind
32+
the features and complete use cases built end to end, have a look at the `talks
33+
and articles <https://jsonargparse.readthedocs.io/en/latest/talks-and-articles.html>`__
34+
page, which collects presentations, blog posts and example projects.
35+
3036
Teaser examples
3137
---------------
3238

jsonargparse/_actions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Collection of useful actions to define arguments."""
22

3+
import inspect
34
import os
45
import re
56
import sys
@@ -409,6 +410,8 @@ def resolve_help_type(self, value, option_string):
409410
raise TypeError(f"{option_string}: {ex}") from ex
410411
if not any(is_subclass(val_class, b) or implements_protocol(val_class, b) for b in class_types):
411412
raise TypeError(f'{option_string}: "{value}" is not a {self._kind} {self._basename}')
413+
if not inspect.isclass(val_class): # a function that implements a callable protocol
414+
raise TypeError(f'{option_string}: "{value}" is not a class, so it has no help')
412415
return val_class
413416

414417
def print_help(self, call_args):

0 commit comments

Comments
 (0)