Skip to content

Commit

Permalink
Implement EnsurePath.for_dataset() and resolve against that dataset
Browse files Browse the repository at this point in the history
This implements DataLad's standard path resolution approach (relative
is relative to CWD, unless a `Dataset` instance is given).

This now makes it possible to perform this standard resolution pattern
during command parameter validation, like so:

```py
\# example validator for a command with two args
EnsureCommandParameterization(
    param_constraints=dict(
        dataset=EnsureDataset(),
        path=EnsurePath(),
    ),
    tailor_for_dataset=dict(path='dataset'),
)
```

This will:

- cause the 'dataset' validator to run first
- auto-generate an `EnsurePath` variant that resolves against that
  dataset
- ensures that the command always receives absolute paths, resolved
  against the dataset whenever the rules require it.

Closes datalad#270
  • Loading branch information
mih committed Mar 3, 2023
1 parent ac10fcb commit e9dbc13
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 8 deletions.
61 changes: 53 additions & 8 deletions datalad_next/constraints/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Basic constraints for declaring essential data types, values, and ranges"""

from __future__ import annotations

__docformat__ = 'restructuredtext'

from pathlib import Path
import re

from .base import Constraint
from datalad_next.datasets import resolve_path

from .base import (
Constraint,
DatasetParameter,
)
from .utils import _type_str


Expand Down Expand Up @@ -332,12 +339,14 @@ class EnsurePath(Constraint):
or relative.
"""
def __init__(self,
*,
path_type: type = Path,
is_format: str or None = None,
lexists: bool or None = None,
is_mode: callable = None,
ref: Path = None,
ref_is: str = 'parent-or-same-as'):
ref_is: str = 'parent-or-same-as',
dsarg: DatasetParameter | None = None):
"""
Parameters
----------
Expand All @@ -362,6 +371,12 @@ def __init__(self,
comparison operation is given by `ref_is`.
ref_is: {'parent-or-identical'}
Comparison operation to perform when `ref` is given.
dsarg: DatasetParameter, optional
If given, incoming paths are resolved in the following fashion:
If, and only if, the original "dataset" parameter was a
``Dataset`` object instance, relative paths are interpreted as
relative to the given dataset. In all other cases, relative paths
are treated as relative to the current working directory.
"""
super().__init__()
self._path_type = path_type
Expand All @@ -370,9 +385,29 @@ def __init__(self,
self._is_mode = is_mode
self._ref = ref
self._ref_is = ref_is
self._dsarg = dsarg

def __call__(self, value):
# turn it into the target type to make everything below
# more straightforward
path = self._path_type(value)

# we are testing the format first, because resolve_path()
# will always turn things into absolute paths
if self._is_format is not None:
is_abs = path.is_absolute()
if self._is_format == 'absolute' and not is_abs:
raise ValueError(f'{path} is not an absolute path')
elif self._is_format == 'relative' and is_abs:
raise ValueError(f'{path} is not a relative path')

# resolve relative paths against a dataset, if given
if self._dsarg:
path = resolve_path(
path,
self._dsarg.original,
self._dsarg.ds)

mode = None
if self._lexists is not None or self._is_mode is not None:
try:
Expand All @@ -385,12 +420,6 @@ def __call__(self, value):
raise ValueError(f'{path} does not exist')
elif not self._lexists and mode is not None:
raise ValueError(f'{path} does (already) exist')
if self._is_format is not None:
is_abs = path.is_absolute()
if self._is_format == 'absolute' and not is_abs:
raise ValueError(f'{path} is not an absolute path')
elif self._is_format == 'relative' and is_abs:
raise ValueError(f'{path} is not a relative path')
if self._is_mode is not None:
if not self._is_mode(mode):
raise ValueError(f'{path} does not match desired mode')
Expand All @@ -408,6 +437,22 @@ def __call__(self, value):
f'{self._ref} is not {self._ref_is} {path}')
return path

def for_dataset(self, dataset: DatasetParameter) -> Constraint:
"""Return an similarly parametrized variant that resolves
paths against a given dataset (argument)
"""
return self.__class__(
path_type=self._path_type,
is_format=self._is_format,
lexists=self._lexists,
is_mode=self._is_mode,
ref=self._ref,
ref_is=self._ref_is,
dsarg=dataset,
)

def short_description(self):
return '{}{}path{}'.format(
'existing '
Expand Down
20 changes: 20 additions & 0 deletions datalad_next/constraints/tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import pathlib
import pytest

from datalad_next.datasets import Dataset

from ..base import DatasetParameter
from ..basic import (
EnsureInt,
EnsureFloat,
Expand Down Expand Up @@ -299,3 +302,20 @@ def test_EnsurePath(tmp_path):
c = EnsurePath(ref=target, ref_is='stupid')
with pytest.raises(ValueError):
c('doesnotmatter')


def test_EnsurePath_fordataset(tmp_path):
P = pathlib.Path
ds = Dataset(tmp_path).create(result_renderer='disabled')
# standard: relative in, relative out
c = EnsurePath()
assert c('relpath') == P('relpath')
# tailor constraint for our dataset
# (this is what would be done by EnsureCommandParameterization
# 1. dataset given as a path -- resolve against CWD
# output is always absolute
tc = c.for_dataset(DatasetParameter(tmp_path, ds))
assert tc('relpath') == (P.cwd() / 'relpath')
# 2. dataset is given as a dataset object
tc = c.for_dataset(DatasetParameter(ds, ds))
assert tc('relpath') == (ds.pathobj / 'relpath')

0 comments on commit e9dbc13

Please sign in to comment.