Skip to content
Open
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
53 changes: 45 additions & 8 deletions confuse/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,23 +323,60 @@ def get(self, template: object = templates.REQUIRED) -> Any:

# Shortcuts for common templates.

def as_filename(self) -> str:
"""Get the value as a path. Equivalent to `get(Filename())`."""
return self.get(templates.Filename())
def as_filename(
self,
*,
cwd: str | None = None,
in_app_dir: bool = False,
in_source_dir: bool = False,
) -> str:
"""Get the value as a filename string.

Path resolution options are passed to the `Filename` template.
"""
return self.get(
templates.Filename(
cwd=cwd, in_app_dir=in_app_dir, in_source_dir=in_source_dir
)
)

def as_path(self) -> Path:
"""Get the value as a `pathlib.Path` object. Equivalent to `get(Path())`."""
return self.get(templates.Path())
def as_path(
self,
*,
cwd: str | None = None,
in_app_dir: bool = False,
in_source_dir: bool = False,
) -> Path:
"""Get the value as a `pathlib.Path` object.

def as_optional_path(self) -> Path | None:
Path resolution options are passed to the `Filename` template.
"""
return self.get(
templates.Path(cwd=cwd, in_app_dir=in_app_dir, in_source_dir=in_source_dir)
)

def as_optional_path(
self,
*,
cwd: str | None = None,
in_app_dir: bool = False,
in_source_dir: bool = False,
) -> Path | None:
"""Get the value as a `pathlib.Path` object or `None`.

If the value is an empty string or `None`, then `None` is returned.
Path resolution options are passed to the `Filename` template.
"""
if self.get(templates.Template(default=None)) == "":
return None

return self.get(templates.Optional(templates.Path()))
return self.get(
templates.Optional(
templates.Path(
cwd=cwd, in_app_dir=in_app_dir, in_source_dir=in_source_dir
)
)
)

def as_choice(self, choices: Sequence[R] | dict[str, R] | type[R]) -> R:
"""Get the value from a list of choices. Equivalent to
Expand Down
8 changes: 5 additions & 3 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ below!
Unreleased
----------

..
New features
~~~~~~~~~~~~
New features
~~~~~~~~~~~~

- Add ``cwd``, ``in_app_dir``, and ``in_source_dir`` keyword arguments to
``as_filename()``, ``as_path()``, and ``as_optional_path()``.

..
Bug fixes
Expand Down
17 changes: 14 additions & 3 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,16 @@ that perform fancier validation or even conversion:
file was loaded with the ``base_for_paths`` parameter set to ``True`` (see
:ref:`manually-specifying-config-files`), then a relative path refers to the
directory containing the config file. A relative path from any other source
(e.g., command-line options) is relative to the working directory. For full
control over relative path resolution, use the ``Filename`` template directly
(see :ref:`filename-template`).
(e.g., command-line options) is relative to the working directory. Pass
``cwd``, ``in_app_dir``, or ``in_source_dir`` as keyword arguments to override
this behavior, as with the ``Filename`` template (see
:ref:`filename-template`). For example,
``config['path'].as_filename(in_source_dir=True)`` resolves a relative path
against the directory containing its source config file.
- ``as_path()``: Like ``as_filename()``, but returns a ``pathlib.Path`` object.
Accepts the same path resolution keyword arguments.
- ``as_optional_path()``: Like ``as_path()``, but returns ``None`` for missing
or null values. Accepts the same path resolution keyword arguments.
- ``as_choice(choices)``: Check that a value is one of the provided choices. The
argument should be a sequence of possible values. If the sequence is a
``dict``, then this method returns the associated value instead of the key.
Expand All @@ -131,6 +138,10 @@ that perform fancier validation or even conversion:
- ``as_str_expanded()``: Expand any environment variables contained in a string
using `os.path.expandvars()`_.

The ``relative_to`` option needs a mapping template containing the sibling
values being validated. Use ``get()`` with a mapping of ``Filename`` or ``Path``
templates for this option instead of a single-value accessor.

.. _os.path.expandvars(): https://docs.python.org/3/library/os.path.html#os.path.expandvars

For example, ``config['path'].as_filename()`` ensures that you get a reasonable
Expand Down
56 changes: 56 additions & 0 deletions test/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,59 @@ def test_as_str_expanded(self):
def test_as_pairs(self):
config = _root({"k": [{"a": "A"}, "b", ["c", "C"]]})
assert [("a", "A"), ("b", None), ("c", "C")] == config["k"].as_pairs()


@pytest.mark.parametrize("accessor", ["as_filename", "as_path", "as_optional_path"])
@pytest.mark.parametrize("option", ["cwd", "in_app_dir", "in_source_dir"])
def test_path_accessor_resolution_options(tmp_path, monkeypatch, accessor, option):
app_dir = tmp_path / "app"
app_dir.mkdir()
monkeypatch.setenv("ACCESSORTESTDIR", str(app_dir))
source_dir = tmp_path / "source"
source_dir.mkdir()
source = source_dir / "config.yaml"
source.write_text("path: nested/file.txt\n", encoding="utf-8")
config = confuse.Configuration("AccessorTest", read=False)
config.set_file(str(source))
options = {option: str(tmp_path / "working") if option == "cwd" else True}
expected_base = {
"cwd": tmp_path / "working",
"in_app_dir": app_dir,
"in_source_dir": source_dir,
}[option]
expected = expected_base / "nested" / "file.txt"
result = getattr(config["path"], accessor)(**options)
assert result == (str(expected) if accessor == "as_filename" else expected)


@pytest.mark.parametrize("value", [None, "", {}])
def test_optional_path_options_preserve_missing_null_and_empty(value, tmp_path):
config = _root({} if value == {} else {"path": value})
assert config["path"].as_optional_path(cwd=str(tmp_path)) is None


@pytest.mark.parametrize("accessor", ["as_filename", "as_path", "as_optional_path"])
def test_path_accessor_precedence_and_updates(tmp_path, monkeypatch, accessor):
app_dir = tmp_path / "app"
app_dir.mkdir()
monkeypatch.setenv("ACCESSORTESTDIR", str(app_dir))
source = tmp_path / "config.yaml"
source.write_text("path: file.txt\n", encoding="utf-8")
config = confuse.Configuration("AccessorTest", read=False)
config.set_file(str(source))
get_path = getattr(config["path"], accessor)
working = tmp_path / "working"
assert (
pathlib.Path(get_path(cwd=str(working), in_app_dir=True, in_source_dir=True))
== working / "file.txt"
)
assert (
pathlib.Path(get_path(in_app_dir=True, in_source_dir=True))
== tmp_path / "file.txt"
)
absolute = tmp_path / "absolute.txt"
config["path"].set(str(absolute))
assert pathlib.Path(get_path(cwd=str(working))) == absolute
config["path"].set(123)
with pytest.raises(confuse.ConfigTypeError):
get_path(cwd=str(working))