Skip to content
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
6 changes: 5 additions & 1 deletion doc/changes/unreleased.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# Unreleased

## Features

* #271: Added method `relative_to` to interface `PathLike`

## Bug Fixing

* #262: Fixed a wrong type interpretation in `path.write`. The chunks are now passed as a Sequence, not Iterable.

## Internal

* #260: Re-locked transitive dependencies urllib3, filelock, and Werkzeug and update to exasol-toolbox 4.0.0
* #260: Re-locked transitive dependencies urllib3, filelock, and Werkzeug and update to exasol-toolbox 4.0.0
32 changes: 32 additions & 0 deletions exasol/bucketfs/_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from typing import (
BinaryIO,
Protocol,
cast,
)

from exasol.saas.client.api_access import get_database_id
Expand Down Expand Up @@ -72,6 +73,20 @@ def parent(self) -> PathLike:
The logical parent of this path.
"""

def relative_to(self, other: PathLike) -> PurePath:
"""
Return an instance of PurePath interpreting this part relative to
the specified ``other``.

Note: ``other`` should be an instance of the same class implementing
the PathLike interface as the current one, e.g. BucketPath.

Example:
a = build_path(..., path="parent")
b = build_path(..., path="parent/child")
assert b.relative(a) == PurePath("child")
"""

def as_uri(self) -> str:
"""
Represent the path as a file URI. Can be used to reconstruct the location/path.
Expand Down Expand Up @@ -331,6 +346,23 @@ def root(self) -> str:
def parent(self) -> PathLike:
return BucketPath(self._path.parent, self._bucket_api)

def relative_to(self, other: PathLike) -> PurePath:
if not isinstance(other, BucketPath):
raise BucketFsError(
"BucketPath.relative_to() called with other"
f" being an instance of {type(other)}."
)
other_bucket_path = cast(BucketPath, other)
if self._bucket_api != other_bucket_path.bucket_api:
raise BucketFsError(
"BucketPath.relative_to() called with other"
f" from a foreign bucket {other_bucket_path._bucket_api}."
)
try:
return self._path.relative_to(other_bucket_path._path)
except ValueError as ex:
raise BucketFsError(repr(ex)) from ex

def as_uri(self) -> str:
return self._path.as_uri()

Expand Down
49 changes: 48 additions & 1 deletion test/unit/test_bucket_path.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from itertools import chain
from pathlib import Path
from pathlib import (
Path,
PurePath,
)

import pytest

Expand Down Expand Up @@ -234,3 +237,47 @@ def test_eq(bucket_fake, file1, file2, expectation):
a = path / file1
b = path / file2
assert (a == b) == expectation


class NotBucketPath(bfs.path.PathLike):
pass


def test_relative_not_bucket_path(bucket_fake):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference of this test compared to test_foreign_bucket?

Copy link
Author

@ckunki ckunki Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test deliberately uses an instance of class NotBucketPath instead of bfs.path.BucketPath to verify the if statement in the implementation.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, but both test basically tes tthe if statement at line 347:

        if self._bucket_api != other.bucket_api:
            raise BucketFsError(
                "BucketPath.relative_to() called with other"
                f" from a foreign bucket {other._bucket_api}."
            )

=> Also the expected error message is probably the same.
Or do I get something wrong...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many thanks for being persistent on this.
I finally found the problem and (hopefully) fixed it.
Please review again.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now it makes sense

root = NotBucketPath()
testee = bfs.path.BucketPath("testee", bucket_api=bucket_fake)
with pytest.raises(
bfs.path.BucketFsError,
match="called with other being an instance of .*NotBucketPath",
):
testee.relative_to(root)


def test_relative_outside(bucket_fake):
a = bfs.path.BucketPath("a", bucket_api=bucket_fake)
b = bfs.path.BucketPath("b", bucket_api=bucket_fake)
with pytest.raises(
bfs.path.BucketFsError, match="'b' is not in the subpath of 'a'"
):
b.relative_to(a)


def test_foreign_bucket(tmp_path, bucket_fake):
api2 = bfs._buckets.MountedBucket(base_path=tmp_path)
foreign = bfs.path.BucketPath("root", bucket_api=api2)
testee = bfs.path.BucketPath("root", bucket_api=bucket_fake)
with pytest.raises(bfs.path.BucketFsError, match="other from a foreign bucket"):
testee.relative_to(foreign)


@pytest.mark.parametrize(
"path, expected_relative",
[
("a", "a"),
("a/b", "a/b"),
],
)
def test_relative_to(path, expected_relative, bucket_fake):
root = bfs.path.BucketPath("root", bucket_api=bucket_fake)
testee = root / path
assert testee.relative_to(root) == PurePath(expected_relative)