Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise error on underspecified path for import/export and view #666

Merged
merged 35 commits into from
Jan 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
13d0904
copy first commit
cbkerr Jan 21, 2022
87f95c1
Switch print statement to error for final
cbkerr Nov 3, 2021
353ad65
Clean up thinking code
cbkerr Nov 3, 2021
3aacdba
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 3, 2021
79a3c86
invert links dictionary internally
cbkerr Nov 3, 2021
7c52173
revert back to old mapping code in linked_view
cbkerr Nov 4, 2021
9db669d
Update signac/contrib/import_export.py
cbkerr Nov 4, 2021
a5ae30d
Apply suggestions from code review
cbkerr Nov 17, 2021
8baed36
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 17, 2021
183a5a1
switch back to RuntimeError for tests and bc type of input is correct
cbkerr Nov 18, 2021
c604904
make path correction change work on all oses
cbkerr Nov 18, 2021
8ee081b
don't log if pathspec is okay
cbkerr Nov 23, 2021
f95156f
add error to docstring
cbkerr Nov 23, 2021
4ace03e
apply suggestions from code review
cbkerr Nov 23, 2021
6c7b27d
Move path function checking to helper function
cbkerr Nov 23, 2021
32aafe1
Do quick path check first
cbkerr Nov 23, 2021
92d2331
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 23, 2021
585c5f9
Pass path string for error message
cbkerr Nov 30, 2021
26e9f8a
add path to suggested fix
cbkerr Jan 21, 2022
e6a4d91
update changelog
cbkerr Dec 7, 2021
f75b0ae
Add test that catches the problem
cbkerr Dec 7, 2021
c85c9ff
WIP testing of view shell command
cbkerr Dec 7, 2021
33e5712
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 7, 2021
52c897f
Update tests/test_shell.py
bdice Dec 10, 2021
eb7d423
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 21, 2022
4dbefb9
Update changelog.txt
cbkerr Jan 21, 2022
41c4cdd
add runtime error back to _export_jobs docstring
cbkerr Jan 21, 2022
bb6ccf8
Avoid duplicate calls to path_function
cbkerr Jan 22, 2022
4c007ca
properly use Counter
cbkerr Jan 22, 2022
6e29705
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 22, 2022
12f7480
Minor fixes.
bdice Jan 23, 2022
fa86a4e
Rename function to indicate the check being performed.
bdice Jan 23, 2022
e7d51e3
Fix iteration over items and separate failure case into its own test.
bdice Jan 23, 2022
335afa6
Update comment.
bdice Jan 23, 2022
30916e9
Clarify comment.
bdice Jan 23, 2022
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
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Fixed
+++++

- ``H5Store.mode`` returns the file mode (#607).
- User-provided path functions now raise an error if not unique (#666).

[1.7.0] -- 2021-06-08
---------------------
Expand Down
48 changes: 41 additions & 7 deletions signac/contrib/import_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import shutil
import tarfile
import zipfile
from collections import OrderedDict
from collections import Counter, OrderedDict
from contextlib import closing, contextmanager
from string import Formatter
from tempfile import TemporaryDirectory
Expand Down Expand Up @@ -149,6 +149,40 @@ class _SchemaPathEvaluationError(RuntimeError):
pass


def _check_path_function_unique(jobs, path_spec, path_function):
"""Validate that path function specifies a 1-1 mapping.

Parameters
----------
jobs : iterable of :class:`~signac.contrib.job.Job`
A sequence of jobs (instances of :class:`~signac.contrib.job.Job`).
path_spec : str
The path string that generated the path_function. Displayed in the
error message.
path_function : callable
A callable path generating function.

Raises
------
RuntimeError
If paths generated with given path function are not unique.

"""
job_paths = Counter(path_function(job) for job in jobs)
duplicates = {path for path, count in job_paths.items() if count > 1}
if len(duplicates) > 0:
# Log paths generated more than once
for path in duplicates:
logger.debug(f"Generated path '{path}' is not unique.")
raise RuntimeError(
f"The path specification '{path_spec}' would result in duplicate "
"paths. See the debug log for the list of duplicate paths. The "
"easiest way to fix this is to append the job id to the path "
"specification like "
f"'{os.path.join(str(path_spec), 'id', '{job.id}')}'."
)


def _make_path_function(jobs, path):
"""Generate a path function for jobs or use ``path`` if it is callable.

Expand Down Expand Up @@ -194,7 +228,7 @@ def path_function(job):
return str(job.id)

elif isinstance(path, str):
# Detect keys that are already provided as part of the path specifier and
# Detect keys that are already provided as part of the path specifier
# and should therefore be excluded from the 'auto'-part.
exclude_keys = [x[1] for x in Formatter().parse(path)]

Expand Down Expand Up @@ -241,6 +275,9 @@ def path_function(job):
except Exception as error:
raise _SchemaPathEvaluationError(error)

# Check that the user-specified path generates a 1-1 mapping
_check_path_function_unique(jobs, path_spec=path, path_function=path_function)

else:
raise ValueError(
"The path argument must either be `None`, `False`, or of type `str`."
Expand Down Expand Up @@ -297,21 +334,18 @@ def _export_jobs(jobs, path, copytree):
------
RuntimeError
If paths generated with given path function are not unique.

"""
# Transform the path argument into a callable if necessary.
if callable(path):
path_function = path
_check_path_function_unique(jobs, path_spec=path, path_function=path_function)
else:
path_function = _make_path_function(jobs, path)
# path_function is checked for uniqueness inside _make_path_function

# Determine export path for each job.
paths = {job.workspace(): path_function(job) for job in jobs}

# Check whether the mapped paths are unique.
if len(set(paths.values())) != len(paths):
raise RuntimeError("Paths generated with given path function are not unique!")

# Check leaf/node consistency
_check_directory_structure_validity(paths.values())

Expand Down
2 changes: 1 addition & 1 deletion signac/contrib/linked_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ def create_linked_view(project, prefix=None, job_ids=None, index=None, path=None
links["./job"] = job.workspace()
assert len(links) < 2
_check_directory_structure_validity(links.keys())

_update_view(prefix, links)

return links


Expand Down
23 changes: 23 additions & 0 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2003,6 +2003,12 @@ def test_create_linked_view_homogeneous_schema_nested_provide_partial_path(self)
sp = {"a": a, "d": {"b": b, "c": c}}
self.project.open_job(sp).init()

# Should error if user-provided path doesn't make 1-1 mapping
with pytest.raises(RuntimeError):
self.project.create_linked_view(
prefix=view_prefix, path=os.path.join("a", "{a}")
)

self.project.create_linked_view(
prefix=view_prefix, path="a/{a}/d.c/{d.c}/{{auto}}"
)
Expand Down Expand Up @@ -2230,6 +2236,23 @@ def test_create_linked_view_with_slash_raises_error(self):
with pytest.raises(RuntimeError):
self.project.create_linked_view(prefix=view_prefix)

@pytest.mark.skipif(WINDOWS, reason="Linked views unsupported on Windows.")
def test_create_linked_view_duplicate_paths(self):
view_prefix = os.path.join(self._tmp_pr, "view")
a_vals = range(2)
b_vals = range(3, 8)
for a in a_vals:
for b in b_vals:
sp = {"a": a, "b": b}
self.project.open_job(sp).init()

# An error should be raised if the user-provided path function doesn't
# make a 1-1 mapping.
with pytest.raises(RuntimeError):
self.project.create_linked_view(
prefix=view_prefix, path=os.path.join("a", "{a}")
)


class UpdateCacheAfterInitJob(signac.contrib.job.Job):
def init(self, *args, **kwargs):
Expand Down
18 changes: 18 additions & 0 deletions tests/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,24 @@ def test_view(self):
"view/a/{}/job".format(sp["a"])
) == os.path.realpath(project.open_job(sp).workspace())

@pytest.mark.skipif(WINDOWS, reason="Symbolic links are unsupported on Windows.")
def test_view_incomplete_path_spec(self):
self.call("python -m signac init my_project".split())
project = signac.Project()
sps = [{"a": i} for i in range(3)]
for sp in sps:
project.open_job(sp).init()
os.mkdir("view")

# An error should be raised if the user-provided path function
# doesn't make a 1-1 mapping.
err = self.call(
"python -m signac view view non_unique".split(),
error=True,
raise_error=False,
)
assert "duplicate paths" in err

def test_find(self):
self.call("python -m signac init my_project".split())
project = signac.Project()
Expand Down