From 56bf77d859eb2d2b5a01cbd22169cada670d3e9f Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:34:43 +0200 Subject: [PATCH] Deprecate Path.get_content in favor of a text-only Path.read_text --- CHANGELOG.rst | 3 ++ DOCUMENTATION.rst | 8 ++-- jsonargparse/_core.py | 6 +-- jsonargparse/_deprecated.py | 31 +++++++++++++ jsonargparse/_from_config.py | 2 +- jsonargparse/_jsonnet.py | 2 +- jsonargparse/_paths.py | 15 +++---- jsonargparse/_typehints.py | 2 +- jsonargparse/_util.py | 2 +- jsonargparse_tests/test_core.py | 2 +- jsonargparse_tests/test_deprecated.py | 63 +++++++++++++++++++++++++++ jsonargparse_tests/test_paths.py | 22 +++++----- 12 files changed, 127 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5e56ce29..66f97d15 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -44,6 +44,9 @@ Deprecated deprecated and will be removed in v5.0.0. Instead run ``.keys()`` and then sort or get the parent and leaf separately. (`#900 `__). +- ``Path.get_content`` is deprecated and will be removed in v5.0.0. Instead use + ``Path.read_text`` for text and ``Path.open`` for binary content (`#906 + `__). v4.48.0 (2026-04-10) diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index b4404870..ca700f32 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -651,8 +651,8 @@ actual path, thus for the previous example: '/.../app/data/info.db' The content of a file referenced by a :class:`.Path` instance can be read using -the :py:meth:`.Path.get_content` method. For the previous example, this would be -``info_db = cfg.databases.info.get_content()``. +the :py:meth:`.Path.read_text` method. For the previous example, this would be +``info_db = cfg.databases.info.read_text()``. An argument with a path type can be given ``nargs='+'`` to parse multiple paths. Thus, from command line you could do ``--files file1 file2``, separated by @@ -721,7 +721,7 @@ Parsing URLs ------------ The :func:`.path_type` function also supports URLs which after parsing, the -:py:meth:`.Path.get_content` method can be used to perform a GET request to the +:py:meth:`.Path.read_text` method can be used to perform a GET request to the corresponding URL and retrieve its content. For this to work the *requests* Python package is required. Alternatively, :func:`.path_type` can also be used for `fsspec `__ supported file systems. @@ -735,7 +735,7 @@ either a readable file or URL, the type would be created as ``Path_fur = path_type('fur')``. If the value appears to be a URL, a HEAD request would be triggered to check if it is accessible. To get the content of the parsed path, without needing to care if it is a local file or a URL, the -:py:meth:`.Path.get_content` method can be used. +:py:meth:`.Path.read_text` method can be used. If you import ``from jsonargparse import set_parsing_settings`` and then run ``set_parsing_settings(config_read_mode_urls_enabled=True)`` or diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index 44deef7a..64dacd1d 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -617,7 +617,7 @@ def parse_path( """ fpath = Path(cfg_path, mode=_get_config_read_mode()) with change_to_path_dir(fpath): - cfg_str = fpath.get_content() + cfg_str = fpath.read_text() parsed_cfg = self.parse_string( cfg_str=cfg_str, cfg_path=os.path.basename(cfg_path), @@ -950,7 +950,7 @@ def save_paths(cfg): val_path = Path(os.path.basename(val.absolute), mode="fc") check_overwrite(val_path) with open(val_path.absolute, "w") as f: - f.write(val.get_content()) + f.write(val.read_text()) cfg[key] = type(val)(str(val_path)) with change_to_path_dir(path_fc), parser_context(parent_parser=self): @@ -1024,7 +1024,7 @@ def get_defaults(self, skip_validation: bool = False, **kwargs) -> Namespace: default_config_files = self._get_default_config_files() for default_config_file in default_config_files: - default_config_file_content = default_config_file.get_content() + default_config_file_content = default_config_file.read_text() if not default_config_file_content.strip(): continue with change_to_path_dir(default_config_file), parser_context(parent_parser=self, parsing_defaults=True): diff --git a/jsonargparse/_deprecated.py b/jsonargparse/_deprecated.py index 611830ee..622fd333 100644 --- a/jsonargparse/_deprecated.py +++ b/jsonargparse/_deprecated.py @@ -466,6 +466,12 @@ def path_skip_check_deprecation(stacklevel=2): ``absolute`` or ``relative`` properties instead. """ +path_get_content_message = """ + ``Path.get_content`` was deprecated in v4.49.0 and will be removed in + v5.0.0. Instead use ``Path.read_text`` for text and ``Path.open`` for binary + data. +""" + class PathDeprecations: """Deprecated methods for Path.""" @@ -524,6 +530,31 @@ def skip_check(self, skip_check): def __call__(self, absolute: bool = True) -> str: return self._absolute if absolute else self._relative + def get_content(self, mode: str = "r"): + deprecation_warning("Path.get_content", path_get_content_message) + if self._std_io: # type: ignore[attr-defined] + from ._paths import _read_cached_stdin + + return _read_cached_stdin() + elif self._is_url: # type: ignore[attr-defined] + from ._optionals import import_requests + + assert mode == "r" + requests = import_requests("Path.get_content") + response = requests.get(self._absolute) + response.raise_for_status() + return response.text + elif self._is_fsspec: # type: ignore[attr-defined] + from ._optionals import import_fsspec + + fsspec = import_fsspec("Path.get_content") + with fsspec.open(self._absolute, mode) as handle: + with handle as input_file: + return input_file.read() + else: + with open(self._absolute, mode) as input_file: + return input_file.read() + @deprecated(""" usage_and_exit_error_handler was deprecated in v4.20.0 and will be removed diff --git a/jsonargparse/_from_config.py b/jsonargparse/_from_config.py index d9012704..bd30a69e 100644 --- a/jsonargparse/_from_config.py +++ b/jsonargparse/_from_config.py @@ -65,7 +65,7 @@ def _parse_class_kwargs_from_config(cls: Type[T], config: Union[str, PathLike, d from .typing import Path cfg_path = Path(config, mode=_get_config_read_mode()) - cfg_str = cfg_path.get_content() + cfg_str = cfg_path.read_text() with parser_context(load_value_mode=parser.parser_mode): try: config = load_value(cfg_str, path=str(config)) diff --git a/jsonargparse/_jsonnet.py b/jsonargparse/_jsonnet.py index ba3d328b..0a1966e2 100644 --- a/jsonargparse/_jsonnet.py +++ b/jsonargparse/_jsonnet.py @@ -163,7 +163,7 @@ def parse( pass else: fname = jsonnet(absolute=False) if isinstance(jsonnet, Path) else jsonnet - snippet = fpath.get_content() + snippet = fpath.read_text() try: with parser_context(load_value_mode="yaml" if pyyaml_available else "json"): values = load_value(_jsonnet.evaluate_snippet(fname, snippet, ext_vars=ext_vars, ext_codes=ext_codes)) diff --git a/jsonargparse/_paths.py b/jsonargparse/_paths.py index 7cad60e2..229f5add 100644 --- a/jsonargparse/_paths.py +++ b/jsonargparse/_paths.py @@ -285,23 +285,22 @@ def __eq__(self, other: Any) -> bool: return str(self) == other return False - def get_content(self, mode: str = "r") -> str: - """Returns the contents of the file or the remote path.""" + def read_text(self) -> str: + """Returns the text contents of the file or the remote path.""" if self._std_io: return _read_cached_stdin() elif self._is_url: - assert mode == "r" - requests = import_requests("Path.get_content") + requests = import_requests("Path.read_text") response = requests.get(self._absolute) response.raise_for_status() return response.text elif self._is_fsspec: - fsspec = import_fsspec("Path.get_content") - with fsspec.open(self._absolute, mode) as handle: + fsspec = import_fsspec("Path.read_text") + with fsspec.open(self._absolute, "r") as handle: with handle as input_file: return input_file.read() else: - with open(self._absolute, mode) as input_file: + with open(self._absolute) as input_file: return input_file.read() @contextmanager @@ -313,7 +312,7 @@ def open(self, mode: str = "r") -> Iterator[IO]: elif "w" in mode: yield sys.stdout elif self._is_url: - yield StringIO(self.get_content()) + yield StringIO(self.read_text()) elif self._is_fsspec: fsspec = import_fsspec("Path.open") with fsspec.open(self._absolute, mode) as handle: diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py index 7b4bc2d6..68e8ea24 100644 --- a/jsonargparse/_typehints.py +++ b/jsonargparse/_typehints.py @@ -948,7 +948,7 @@ def adapt_typehints( from ._optionals import _get_config_read_mode list_path = Path(val, mode=_get_config_read_mode()) - val = list_path.get_content().splitlines() + val = list_path.read_text().splitlines() if isinstance(val, NestedArg) and subtypehints is not None: val = (prev_val[:-1] if isinstance(prev_val, list) else []) + [val] elif isinstance(val, Iterable) and not isinstance(val, (list, str)) and type(val) not in mapping_origin_types: diff --git a/jsonargparse/_util.py b/jsonargparse/_util.py index 054025f6..02ea39b2 100644 --- a/jsonargparse/_util.py +++ b/jsonargparse/_util.py @@ -116,7 +116,7 @@ def parse_value_or_config( pass else: with cfg_path.relative_path_context(): - value = load_value(cfg_path.get_content(), simple_types=simple_types) + value = load_value(cfg_path.read_text(), simple_types=simple_types) if type(value) is str and value.strip() != "": parsed_val = load_value(value, simple_types=simple_types) if type(parsed_val) is not str: diff --git a/jsonargparse_tests/test_core.py b/jsonargparse_tests/test_core.py index cd931339..3f5e1818 100644 --- a/jsonargparse_tests/test_core.py +++ b/jsonargparse_tests/test_core.py @@ -878,7 +878,7 @@ def test_save_fsspec(example_parser): cfg = example_parser.parse_args(["--nums.val1=5"]) example_parser.save(cfg, "memory://config.yaml", multifile=False) path = path_type("sr")("memory://config.yaml") - assert cfg == example_parser.parse_string(path.get_content()) + assert cfg == example_parser.parse_string(path.read_text()) with pytest.raises(NotImplementedError) as ctx: example_parser.save(cfg, "memory://config.yaml", multifile=True) diff --git a/jsonargparse_tests/test_deprecated.py b/jsonargparse_tests/test_deprecated.py index 48aad50e..8f8524ca 100644 --- a/jsonargparse_tests/test_deprecated.py +++ b/jsonargparse_tests/test_deprecated.py @@ -59,9 +59,11 @@ from jsonargparse_tests.conftest import ( get_parser_help, is_posix, + responses_activate, skip_if_docstring_parser_unavailable, skip_if_fsspec_unavailable, skip_if_requests_unavailable, + skip_if_responses_unavailable, ) from jsonargparse_tests.test_dataclasses import DataClassA from jsonargparse_tests.test_jsonnet import example_2_jsonnet @@ -705,6 +707,67 @@ def test_path_call(paths): # noqa: F811 assert path() == str(paths.tmp_path / paths.file_rw) +def test_file_path_get_content(paths): # noqa: F811 + path = Path(paths.file_r, "fr") + with catch_warnings(record=True) as w: + content = path.get_content() + assert_deprecation_warn( + w, + message="``Path.get_content`` was deprecated", + code="content = path.get_content()", + ) + assert "file contents" == content + + +def test_std_input_path_get_content(): + input_text_to_test = "a text here\n" + with patch("sys.stdin", StringIO(input_text_to_test)), catch_warnings(record=True) as w: + path = Path("-", mode="fr") + assert input_text_to_test == path.get_content() + assert_deprecation_warn( + w, + message="``Path.get_content`` was deprecated", + code="input_text_to_test == path.get_content()", + ) + + +@skip_if_responses_unavailable +@responses_activate +def test_path_url_200(): + import responses + + existing = "http://example.com/existing-url" + existing_body = "url contents" + responses.add(responses.GET, existing, status=200, body=existing_body) + responses.add(responses.HEAD, existing, status=200) + path = Path(existing, mode="ur") + with catch_warnings(record=True) as w: + assert existing_body == path.get_content() + assert_deprecation_warn( + w, + message="``Path.get_content`` was deprecated", + code="existing_body == path.get_content()", + ) + + +@skip_if_fsspec_unavailable +def test_path_fsspec_memory(): + import fsspec + + file_content = "content in memory" + memfile = "memfile.txt" + path = Path(f"memory://{memfile}", mode="sw") + with fsspec.open(path, "w") as f: + f.write(file_content) + with catch_warnings(record=True) as w: + assert file_content == path.get_content() + assert_deprecation_warn( + w, + message="``Path.get_content`` was deprecated", + code="file_content == path.get_content()", + ) + + def test_ActionPathList(tmp_cwd): tmpdir = os.path.join(tmp_cwd, "subdir") os.mkdir(tmpdir) diff --git a/jsonargparse_tests/test_paths.py b/jsonargparse_tests/test_paths.py index 93486702..79f47105 100644 --- a/jsonargparse_tests/test_paths.py +++ b/jsonargparse_tests/test_paths.py @@ -159,10 +159,10 @@ def test_path_dir_access_mode(paths): pytest.raises(TypeError, lambda: Path(paths.file_r, "dr")) -def test_path_get_content(paths): - assert "file contents" == Path(paths.file_r, "fr").get_content() - assert "file contents" == Path(f"file://{paths.tmp_path}/{paths.file_r}", "fr").get_content() - assert "file contents" == Path(f"file://{paths.tmp_path}/{paths.file_r}", "ur").get_content() +def test_path_read_text(paths): + assert "file contents" == Path(paths.file_r, "fr").read_text() + assert "file contents" == Path(f"file://{paths.tmp_path}/{paths.file_r}", "fr").read_text() + assert "file contents" == Path(f"file://{paths.tmp_path}/{paths.file_r}", "ur").read_text() @skip_if_running_as_root @@ -227,13 +227,13 @@ def test_path_tilde_home(paths): assert path.absolute == os.path.join(paths.tmp_path, paths.file_rw) -def test_std_input_path_get_content(): +def test_std_input_path_read_text(): input_text_to_test = "a text here\n" with patch("sys.stdin", StringIO(input_text_to_test)): path = Path("-", mode="fr") assert path == "-" - assert input_text_to_test == path.get_content("r") + assert input_text_to_test == path.read_text() def test_std_input_path_open(): @@ -302,7 +302,7 @@ def test_path_url_200(): responses.add(responses.GET, existing, status=200, body=existing_body) responses.add(responses.HEAD, existing, status=200) path = Path(existing, mode="ur") - assert existing_body == path.get_content() + assert existing_body == path.read_text() @skip_if_responses_unavailable @@ -337,7 +337,7 @@ def test_path_fsspec_zipfile(tmp_cwd): zip2_path.chmod(0) path = Path(f"zip://{existing}::file://{zip1_path}", mode="sr") - assert existing_body == path.get_content() + assert existing_body == path.read_text() with pytest.raises(TypeError) as ctx: Path(f"zip://{nonexisting}::file://{zip1_path}", mode="sr") @@ -356,7 +356,7 @@ def test_path_fsspec_memory(): path = Path(f"memory://{memfile}", mode="sw") with fsspec.open(path, "w") as f: f.write(file_content) - assert file_content == path.get_content() + assert file_content == path.read_text() def test_path_fsspec_invalid_mode(): @@ -431,11 +431,11 @@ def test_relative_path_context_fsspec(tmp_cwd, subtests): with subtests.test("absolute local path"): path0 = Path(local_path, mode="fr") - assert "zero" == path0.get_content() + assert "zero" == path0.read_text() with subtests.test("relative fsspec path"): path1 = Path("../file1.txt", mode="fsr") - assert "one" == path1.get_content() + assert "one" == path1.read_text() assert str(path1) == "../file1.txt" assert path1.absolute == "memory://one/two/file1.txt" assert path1._url_data is not None