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

Support using the subtraction operator to get the relative path between URLs #1340

Merged
merged 27 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c27d7d4
Support using the subtraction operator to get the relative path betwe…
oleksbabieiev Oct 20, 2024
fe047cc
Add CHANGES/1340.feature.rst
oleksbabieiev Oct 20, 2024
81ed86d
Merge branch 'master' into relpath
bdraco Oct 21, 2024
f18ed6b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 21, 2024
97820e5
Merge branch 'master' into relpath
bdraco Oct 21, 2024
d13bf1d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 21, 2024
5037e6b
fix conflicting imports
bdraco Oct 21, 2024
6478fde
Merge branch 'master' into relpath
bdraco Oct 21, 2024
3665a02
Merge branch 'master' into relpath
oleksbabieiev Oct 21, 2024
3add068
Sign CHANGES/1340.feature.rst
oleksbabieiev Oct 21, 2024
38c7f3c
Move `relative_path()` to `_path.py`
oleksbabieiev Oct 21, 2024
4dd4a69
Add more parameters to test `URL.__sub__()`
oleksbabieiev Oct 21, 2024
f5f242b
Use `SEPARATOR` constant for `/`
oleksbabieiev Oct 21, 2024
2006066
Disallow `relative_path()` between abs and rel paths
oleksbabieiev Oct 21, 2024
c717845
Remove the `SEPARATOR` constant
oleksbabieiev Oct 21, 2024
827171f
Rename `relative_path()`
oleksbabieiev Oct 21, 2024
b436b14
Refactor `test_url.py`
oleksbabieiev Oct 21, 2024
1b620df
Refactor `_path.py`
oleksbabieiev Oct 21, 2024
bfb2c4d
Add a small demo to `1340.feature.rst`
oleksbabieiev Oct 21, 2024
2b87478
Update docs for `URL.__sub__()`
oleksbabieiev Oct 21, 2024
05c2147
Add a PEP 257-compliant docstring for `URL.__sub__()`
oleksbabieiev Oct 21, 2024
42c75aa
Update CHANGES/1340.feature.rst
oleksbabieiev Oct 21, 2024
7da1599
Avoid the `os` namespace
oleksbabieiev Oct 22, 2024
e252ff7
Introduce the `offset` variable
oleksbabieiev Oct 22, 2024
1bebdd0
Merge branch 'master' into relpath
oleksbabieiev Oct 22, 2024
39060d1
Replace `PurePath` with `PurePosixPath`
oleksbabieiev Oct 22, 2024
d683e16
Refactor `_path.py`
oleksbabieiev Oct 22, 2024
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
2 changes: 2 additions & 0 deletions CHANGES/1340.feature.rst
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added support for using the :meth:`subtraction operator <yarl.URL.__sub__>`
to get the relative path between URLs -- by :user:`oleksbabieiev`.
15 changes: 15 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,21 @@ The path is encoded if needed.
>>> base.join(URL('//python.org/page.html'))
URL('http://python.org/page.html')

The subtraction (``-``) operator creates a new URL with
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved
a relative *path* to the target URL from the given base URL.
*scheme*, *user*, *password*, *host* and *port* are removed.

.. method:: URL.__sub__(url)

Returns a new URL with a relative *path* between two other URL objects.

.. doctest::

>>> target = URL('http://example.com/path/index.html')
>>> base = URL('http://example.com/')
>>> target - base
URL('path/index.html')
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved

Human readable representation
-----------------------------

Expand Down
46 changes: 46 additions & 0 deletions tests/test_url.py
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,52 @@ def test_str():
assert str(url) == "http://example.com:8888/path/to?a=1&b=2"


@pytest.mark.parametrize(
"target,base,expected",
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved
[
("http://example.com/path/to", "http://example.com/", "path/to"),
("http://example.com/path/to", "http://example.com/spam", "path/to"),
("http://example.com/path/to", "http://example.com/spam/", "../path/to"),
("http://example.com/path", "http://example.com/path/to/", ".."),
("http://example.com/", "http://example.com/", "."),
("http://example.com", "http://example.com", "."),
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved
("http://example.com/", "http://example.com", "."),
("http://example.com", "http://example.com/", "."),
("//example.com", "//example.com", "."),
("/path/to", "/spam/", "../path/to"),
("path/to", "spam/", "../path/to"),
("path/to", "spam", "path/to"),
("..", ".", ".."),
(".", "..", "."),
],
)
def test_sub(target: str, base: str, expected: str):
assert URL(target) - URL(base) == URL(expected)


def test_sub_with_different_schemes():
with pytest.raises(ValueError) as ctx:
URL("http://example.com/") - URL("https://example.com/")
assert "Both URLs should have the same scheme" == str(ctx.value)


def test_sub_with_different_netlocs():
with pytest.raises(ValueError) as ctx:
URL("https://spam.com/") - URL("https://ham.com/")
assert "Both URLs should have the same netloc" == str(ctx.value)
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved

oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved

def test_sub_with_abs_and_rel_paths():
with pytest.raises(ValueError) as ctx:
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved
URL("path/to") - URL("/path/from")
assert (
"It is forbidden to get the path "
"between the absolute and relative paths "
"because it is impossible "
"to get the current working directory." == str(ctx.value)
)


def test_repr():
url = URL("http://example.com")
assert "URL('http://example.com')" == repr(url)
Expand Down
32 changes: 28 additions & 4 deletions yarl/_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from collections.abc import Sequence
from contextlib import suppress
from os.path import dirname, relpath

SEPARATOR = "/"


def normalize_path_segments(segments: Sequence[str]) -> list[str]:
Expand Down Expand Up @@ -31,11 +34,32 @@ def normalize_path_segments(segments: Sequence[str]) -> list[str]:
def normalize_path(path: str) -> str:
# Drop '.' and '..' from str path
prefix = ""
if path and path[0] == "/":
if path and path[0] == SEPARATOR:
# preserve the "/" root element of absolute paths, copying it to the
# normalised output as per sections 5.2.4 and 6.2.2.3 of rfc3986.
prefix = "/"
prefix = SEPARATOR
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved
path = path[1:]

segments = path.split("/")
return prefix + "/".join(normalize_path_segments(segments))
segments = path.split(SEPARATOR)
return prefix + SEPARATOR.join(normalize_path_segments(segments))


def relative_path(path: str, start: str) -> str:
"""A wrapper over os.path.relpath()"""
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved

if not path:
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved
path = SEPARATOR
if not start:
start = SEPARATOR
if not start.endswith(SEPARATOR):
start = dirname(start)

if (path.startswith(SEPARATOR) and not start.startswith(SEPARATOR)) or (
not path.startswith(SEPARATOR) and start.startswith(SEPARATOR)
):
raise ValueError(
"It is forbidden to get the path between the absolute and relative paths "
"because it is impossible to get the current working directory."
)

return relpath(path, start)
17 changes: 16 additions & 1 deletion yarl/_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from propcache.api import under_cached_property as cached_property

from ._parse import USES_AUTHORITY, make_netloc, split_netloc, split_url, unsplit_result
from ._path import normalize_path, normalize_path_segments
from ._path import normalize_path, normalize_path_segments, relative_path
from ._query import (
Query,
QueryVariable,
Expand Down Expand Up @@ -475,6 +475,21 @@ def __truediv__(self, name: str) -> "URL":
return NotImplemented
return self._make_child((str(name),))

def __sub__(self, other: object) -> "URL":
oleksbabieiev marked this conversation as resolved.
Show resolved Hide resolved
if type(other) is not URL:
return NotImplemented

target = self._val
base = other._val

if target.scheme != base.scheme:
raise ValueError("Both URLs should have the same scheme")
if target.netloc != base.netloc:
raise ValueError("Both URLs should have the same netloc")
bdraco marked this conversation as resolved.
Show resolved Hide resolved

path = relative_path(target.path, base.path)
return self._from_tup(("", "", path, "", ""))

def __mod__(self, query: Query) -> "URL":
return self.update_query(query)

Expand Down
Loading