Skip to content

Commit f8e1928

Browse files
authored
FromConfigMixin.from_config now supports fsspec config loading (#918)
1 parent 7f82bf9 commit f8e1928

4 files changed

Lines changed: 62 additions & 11 deletions

File tree

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ paths are considered internals and can change in minor and patch releases.
1515
v4.50.0 (unreleased)
1616
--------------------
1717

18+
Added
19+
^^^^^
20+
- ``FromConfigMixin.from_config`` with ``config_read_mode_fsspec_enabled=True``
21+
now supports fsspec config loading including the resolving of relative paths
22+
(`#918 <https://github.com/omni-us/jsonargparse/pull/918>`__).
23+
1824
Changed
1925
^^^^^^^
2026
- Drop support for Python 3.9. The minimum supported Python version is now

DOCUMENTATION.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -744,10 +744,11 @@ If you import ``from jsonargparse import set_parsing_settings`` and then run
744744
functions and classes will also support loading from URLs: :meth:`parse_path
745745
<.ArgumentParser.parse_path>`, :meth:`get_defaults
746746
<.ArgumentParser.get_defaults>` (``default_config_files`` argument),
747-
``action="config"``, :class:`.ActionJsonSchema`, :class:`.ActionJsonnet` and
748-
:class:`.ActionParser`. This means that a tool that can receive a config file
749-
via ``action="config"`` is able to get the content from a URL, thus something
750-
like the following would work:
747+
``action="config"``, :py:meth:`.FromConfigMixin.from_config`,
748+
:class:`.ActionJsonSchema`, :class:`.ActionJsonnet` and :class:`.ActionParser`.
749+
This means that a tool that can receive a config file via ``action="config"`` is
750+
able to get the content from a URL, thus something like the following would
751+
work:
751752

752753
.. code-block:: bash
753754

jsonargparse/_from_config.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
from ._core import ArgumentParser
99
from ._loaders_dumpers import get_loader_exceptions, load_value
1010
from ._optionals import _get_config_read_mode
11+
from ._paths import change_to_path_dir
1112
from ._required import clear_required, iter_required_keys
1213
from ._typehints import is_subclass_spec, resolve_class_path_by_name
13-
from ._util import import_object
14+
from ._util import import_object, load_config_path_context
1415

1516
__all__ = ["FromConfigMixin"]
1617

@@ -30,7 +31,9 @@ class FromConfigMixin:
3031
defaults.
3132
3233
2. Adds a ``from_config`` ``@classmethod``, that instantiates the class
33-
based on a config file or dict.
34+
based on a config file or dict. If
35+
``config_read_mode_fsspec_enabled=True`` is set, then config paths
36+
can be URLs.
3437
3538
Attributes:
3639
__from_config_init_defaults__: Optional path to a config file for
@@ -61,12 +64,17 @@ def from_config(cls: type[T], config: str | PathLike | dict) -> T:
6164
def _parse_class_kwargs_from_config(cls: type[T], config: str | PathLike | dict, **kwargs) -> tuple[dict, type[T]]:
6265
"""Parse the init kwargs for ``cls`` from a config file or dict."""
6366
parser = ArgumentParser(exit_on_error=False, **kwargs)
67+
cfg_path = None
6468
if not isinstance(config, dict):
6569
from .typing import Path
6670

6771
cfg_path = Path(config, mode=_get_config_read_mode())
68-
cfg_str = cfg_path.read_text()
69-
with parser_context(load_value_mode=parser.parser_mode):
72+
with (
73+
load_config_path_context(cfg_path),
74+
change_to_path_dir(cfg_path),
75+
parser_context(load_value_mode=parser.parser_mode),
76+
):
77+
cfg_str = cfg_path.read_text()
7078
try:
7179
config = load_value(cfg_str, path=str(config))
7280
except get_loader_exceptions() as ex:
@@ -86,7 +94,8 @@ def _parse_class_kwargs_from_config(cls: type[T], config: str | PathLike | dict,
8694
parser.add_class_arguments(cls)
8795
for required in iter_required_keys(parser):
8896
clear_required(parser, required)
89-
cfg = parser.parse_object(config, defaults=False)
97+
with load_config_path_context(cfg_path), change_to_path_dir(cfg_path):
98+
cfg = parser.parse_object(config, defaults=False)
9099
return parser.instantiate(cfg).as_dict(), cls
91100

92101

jsonargparse_tests/test_from_config.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,21 @@
22

33
import pytest
44

5-
from jsonargparse import FromConfigMixin
5+
from jsonargparse import FromConfigMixin, set_parsing_settings
6+
from jsonargparse._optionals import fsspec_support
67
from jsonargparse._paths import PathError
7-
from jsonargparse_tests.conftest import json_or_yaml_dump, skip_if_no_pyyaml, skip_if_omegaconf_unavailable
8+
from jsonargparse.typing import path_type
9+
from jsonargparse_tests.conftest import (
10+
json_or_yaml_dump,
11+
skip_if_fsspec_unavailable,
12+
skip_if_no_pyyaml,
13+
skip_if_omegaconf_unavailable,
14+
)
15+
16+
if fsspec_support:
17+
import fsspec
18+
19+
Path_fsr = path_type("fsr")
820

921
# __init__ defaults override tests
1022

@@ -198,6 +210,29 @@ def test_from_config_method_path(tmp_cwd):
198210
assert instance.parent_param == "value_from_file"
199211

200212

213+
@skip_if_fsspec_unavailable
214+
def test_from_config_method_path_fsspec_relative_paths():
215+
class FromConfigMethodFsspecRelativePath(FromConfigMixin):
216+
def __init__(self, file: Path_fsr):
217+
self.file = file
218+
219+
config_path = "memory://from_config/config.yaml"
220+
file_path = "memory://from_config/file.txt"
221+
with fsspec.open(config_path, "w") as f:
222+
f.write(json_or_yaml_dump({"file": "file.txt"}))
223+
with fsspec.open(file_path, "w") as f:
224+
f.write("value_from_fsspec")
225+
226+
set_parsing_settings(config_read_mode_fsspec_enabled=True)
227+
try:
228+
instance = FromConfigMethodFsspecRelativePath.from_config(config_path)
229+
finally:
230+
set_parsing_settings(config_read_mode_fsspec_enabled=False)
231+
232+
assert instance.file.absolute == file_path
233+
assert instance.file.read_text() == "value_from_fsspec"
234+
235+
201236
def test_from_config_method_path_does_not_exist(tmp_cwd):
202237
config_path = tmp_cwd / "config.yaml"
203238

0 commit comments

Comments
 (0)