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

Normalize path while requesting list of assets from the server #1454

Merged
merged 4 commits into from
Jun 14, 2024
Merged
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
39 changes: 38 additions & 1 deletion dandi/dandiapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import json
import os.path
from pathlib import Path, PurePosixPath
import posixpath
import re
from time import sleep, time
from types import TracebackType
Expand Down Expand Up @@ -937,6 +938,29 @@
client=client, identifier=data["identifier"], version=version, data=data
)

@staticmethod
def _normalize_path(path: str) -> str:
"""
Helper to normalize path before passing it to the server.

We and API call it "path" but it is really a "prefix" with inherent
semantics of containing directory divider '/' and referring to a
directory when terminated with '/'.
"""
# Server (now) expects path to be a proper prefix, so to account for user
# possibly specifying ./ or some other relative paths etc, let's normalize
# the path.
# Ref: https://github.com/dandi/dandi-cli/issues/1452
path_normed = posixpath.normpath(path)
if path_normed == ".":
path_normed = ""

Check warning on line 956 in dandi/dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/dandiapi.py#L956

Added line #L956 was not covered by tests
elif path.endswith("/"):
# we need to make sure that we have a trailing slash if we had it before
path_normed += "/"
if path_normed != path:
lgr.debug("Normalized path %r to %r", path, path_normed)

Check warning on line 961 in dandi/dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/dandiapi.py#L961

Added line #L961 was not covered by tests
return path_normed

def json_dict(self) -> dict[str, Any]:
"""
Convert to a JSONable `dict`, omitting the ``client`` attribute and
Expand Down Expand Up @@ -1154,6 +1178,9 @@
Returns an iterator of all assets in this version of the Dandiset whose
`~RemoteAsset.path` attributes start with ``path``

``path`` is normalized first to possibly remove leading ``./`` or relative
paths (e.g., ``../``) within it.

Assets can be sorted by a given field by passing the name of that field
as the ``order`` parameter. The accepted field names are
``"created"``, ``"modified"``, and ``"path"``. Prepend a hyphen to the
Expand All @@ -1162,7 +1189,7 @@
try:
for a in self.client.paginate(
f"{self.version_api_path}assets/",
params={"path": path, "order": order},
params={"path": self._normalize_path(path), "order": order},
):
yield RemoteAsset.from_data(self, a)
except HTTP404Error:
Expand Down Expand Up @@ -1200,7 +1227,11 @@
Fetch the asset in this version of the Dandiset whose
`~RemoteAsset.path` equals ``path``. If the given asset does not
exist, a `NotFoundError` is raised.

``path`` is normalized first to possibly remove leading ``./`` or relative
paths (e.g., ``../``) within it.
"""
path = self._normalize_path(path)
try:
# Weed out any assets that happen to have the given path as a
# proper prefix:
Expand All @@ -1221,9 +1252,15 @@
"""
Download all assets under the virtual directory ``assets_dirpath`` to
the directory ``dirpath``. Downloads are synchronous.

``path`` is normalized first to possibly remove leading ``./`` or relative
paths (e.g., ``../``) within it.
"""
if assets_dirpath and not assets_dirpath.endswith("/"):
assets_dirpath += "/"
# need to normalize explicitly since we do use it below also
# to deduce portion of the path to strip off.
assets_dirpath = self._normalize_path(assets_dirpath)

Check warning on line 1263 in dandi/dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/dandiapi.py#L1263

Added line #L1263 was not covered by tests
assets = list(self.get_assets_with_path_prefix(assets_dirpath))
for a in assets:
filepath = Path(dirpath, a.path[len(assets_dirpath) :])
Expand Down
66 changes: 45 additions & 21 deletions dandi/tests/test_dandiapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import random
import re
from shutil import rmtree
from typing import Any

import anys
import click
Expand Down Expand Up @@ -46,7 +47,7 @@
d.upload_raw_asset(simple1_nwb, {"path": "testing/simple1.nwb"})
(asset,) = d.get_assets()
assert asset.path == "testing/simple1.nwb"
d.download_directory("", tmp_path)
d.download_directory("./", tmp_path)

Check warning on line 50 in dandi/tests/test_dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/tests/test_dandiapi.py#L50

Added line #L50 was not covered by tests
paths = list_paths(tmp_path)
assert paths == [tmp_path / "testing" / "simple1.nwb"]
assert paths[0].stat().st_size == simple1_nwb.stat().st_size
Expand Down Expand Up @@ -678,26 +679,49 @@


def test_get_assets_with_path_prefix(text_dandiset: SampleDandiset) -> None:
assert sorted(
asset.path
for asset in text_dandiset.dandiset.get_assets_with_path_prefix("subdir")
) == ["subdir1/apple.txt", "subdir2/banana.txt", "subdir2/coconut.txt"]
assert sorted(
asset.path
for asset in text_dandiset.dandiset.get_assets_with_path_prefix("subdir2")
) == ["subdir2/banana.txt", "subdir2/coconut.txt"]
assert [
asset.path
for asset in text_dandiset.dandiset.get_assets_with_path_prefix(
"subdir", order="path"
)
] == ["subdir1/apple.txt", "subdir2/banana.txt", "subdir2/coconut.txt"]
assert [
asset.path
for asset in text_dandiset.dandiset.get_assets_with_path_prefix(
"subdir", order="-path"
)
] == ["subdir2/coconut.txt", "subdir2/banana.txt", "subdir1/apple.txt"]
def _get_assets_with_path_prefix(prefix: str, **kw: Any) -> list[str]:
return [

Check warning on line 683 in dandi/tests/test_dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/tests/test_dandiapi.py#L682-L683

Added lines #L682 - L683 were not covered by tests
asset.path
for asset in text_dandiset.dandiset.get_assets_with_path_prefix(
prefix, **kw
)
]

assert (

Check warning on line 690 in dandi/tests/test_dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/tests/test_dandiapi.py#L690

Added line #L690 was not covered by tests
sorted(_get_assets_with_path_prefix("subdir"))
== sorted(_get_assets_with_path_prefix("./subdir"))
== sorted(_get_assets_with_path_prefix("./subdir2/../sub"))
== [
"subdir1/apple.txt",
"subdir2/banana.txt",
"subdir2/coconut.txt",
]
)
assert (

Check warning on line 700 in dandi/tests/test_dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/tests/test_dandiapi.py#L700

Added line #L700 was not covered by tests
_get_assets_with_path_prefix("subdir/")
== _get_assets_with_path_prefix("./subdir/")
== _get_assets_with_path_prefix("../subdir1/")
== []
)
assert (

Check warning on line 706 in dandi/tests/test_dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/tests/test_dandiapi.py#L706

Added line #L706 was not covered by tests
sorted(_get_assets_with_path_prefix("subdir2"))
== sorted(_get_assets_with_path_prefix("a/../subdir2"))
== sorted(_get_assets_with_path_prefix("./subdir2"))
== [
"subdir2/banana.txt",
"subdir2/coconut.txt",
]
)
assert _get_assets_with_path_prefix("subdir", order="path") == [

Check warning on line 715 in dandi/tests/test_dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/tests/test_dandiapi.py#L715

Added line #L715 was not covered by tests
"subdir1/apple.txt",
"subdir2/banana.txt",
"subdir2/coconut.txt",
]
assert _get_assets_with_path_prefix("subdir", order="-path") == [

Check warning on line 720 in dandi/tests/test_dandiapi.py

View check run for this annotation

Codecov / codecov/patch

dandi/tests/test_dandiapi.py#L720

Added line #L720 was not covered by tests
"subdir2/coconut.txt",
"subdir2/banana.txt",
"subdir1/apple.txt",
]


def test_get_assets_by_glob(text_dandiset: SampleDandiset) -> None:
Expand Down
Loading