Skip to content

Commit

Permalink
Rename URL.__sub__ to URL.relative_to
Browse files Browse the repository at this point in the history
  • Loading branch information
oleksbabieiev committed Dec 27, 2024
1 parent ae99021 commit 4033cb7
Show file tree
Hide file tree
Showing 5 changed files with 57 additions and 49 deletions.
14 changes: 9 additions & 5 deletions CHANGES/1392.feature.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Added support for using the :meth:`subtraction operator <yarl.URL.__sub__>`
Added a new method :py:meth:`~yarl.URL.relative_to`
to get the relative path between URLs.

Note that both URLs must have the same scheme, user, password, host and port:
Expand All @@ -7,16 +7,20 @@ Note that both URLs must have the same scheme, user, password, host and port:
>>> target = URL("http://example.com/path/index.html")
>>> base = URL("http://example.com/")
>>> target - base
>>> target.relative_to(base)
URL('path/index.html')
>>> base.relative_to(target)
URL('..')
URLs can also be relative:

.. code-block:: pycon
>>> target = URL("/")
>>> base = URL("/path/index.html")
>>> target - base
>>> target = URL("/path/index.html")
>>> base = URL("/")
>>> target.relative_to(base)
URL('path/index.html')
>>> base.relative_to(target)
URL('..')
-- by :user:`oleksbabieiev`
6 changes: 4 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ The path is encoded if needed.
>>> base.join(URL('//python.org/page.html'))
URL('http://python.org/page.html')

.. method:: URL.__sub__(url)
.. method:: URL.relative_to(url)

Return a new URL with a relative *path* between two other URL objects.
*scheme*, *user*, *password*, *host*, *port*, *query* and *fragment* are removed.
Expand All @@ -1024,8 +1024,10 @@ The path is encoded if needed.

>>> target = URL('http://example.com/path/index.html')
>>> base = URL('http://example.com/')
>>> target - base
>>> target.relative_to(base)
URL('path/index.html')
>>> base.relative_to(target)
URL('..')

.. versionadded:: 1.19

Expand Down
24 changes: 12 additions & 12 deletions tests/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ def test_str():
(".", "..", "."),
],
)
def test_sub(target: str, base: str, expected: str):
def test_relative_to(target: str, base: str, expected: str):
expected_url = URL(expected)
result_url = URL(target) - URL(base)
result_url = URL(target).relative_to(URL(base))
assert result_url == expected_url


Expand All @@ -114,34 +114,34 @@ def test_sub(target: str, base: str, expected: str):
),
],
)
def test_sub_empty_segments(target: str, base: str, expected: str):
def test_relative_to_with_empty_segments(target: str, base: str, expected: str):
expected_url = URL(expected)
result_url = URL(target) - URL(base)
result_url = URL(target).relative_to(URL(base))
assert result_url == expected_url


def test_sub_with_different_schemes():
def test_relative_to_with_different_schemes():
expected_error_msg = r"^Both URLs should have the same scheme$"
with pytest.raises(ValueError, match=expected_error_msg):
URL("http://example.com/") - URL("https://example.com/")
URL("http://example.com/").relative_to(URL("https://example.com/"))


def test_sub_with_different_netlocs():
def test_relative_to_with_different_netlocs():
expected_error_msg = r"^Both URLs should have the same netloc$"
with pytest.raises(ValueError, match=expected_error_msg):
URL("https://spam.com/") - URL("https://ham.com/")
URL("https://spam.com/").relative_to(URL("https://ham.com/"))


def test_sub_with_different_anchors():
def test_relative_to_with_different_anchors():
expected_error_msg = r"^'path/to' and '/path' have different anchors$"
with pytest.raises(ValueError, match=expected_error_msg):
URL("path/to") - URL("/path/from")
URL("path/to").relative_to(URL("/path/from"))


def test_sub_with_two_dots_in_base():
def test_relative_to_with_two_dots_in_base():
expected_error_msg = r"^'..' segment in '/path/..' cannot be walked$"
with pytest.raises(ValueError, match=expected_error_msg):
URL("path/to") - URL("/path/../from")
URL("path/to").relative_to(URL("/path/../from"))


def test_repr():
Expand Down
8 changes: 4 additions & 4 deletions tests/test_url_benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,18 +614,18 @@ def _run() -> None:
url.query


def test_url_subtract(benchmark: BenchmarkFixture) -> None:
def test_relative_to(benchmark: BenchmarkFixture) -> None:
@benchmark
def _run() -> None:
for _ in range(100):
URL_WITH_LONGER_PATH - URL_WITH_PATH
URL_WITH_LONGER_PATH.relative_to(URL_WITH_PATH)


def test_url_subtract_long_urls(benchmark: BenchmarkFixture) -> None:
def test_relative_to_long_urls(benchmark: BenchmarkFixture) -> None:
@benchmark
def _run() -> None:
for _ in range(100):
URL_VERY_LONG_PATH - URL_LONG_PATH
URL_VERY_LONG_PATH.relative_to(URL_LONG_PATH)


def test_url_host_port_subcomponent(benchmark: BenchmarkFixture) -> None:
Expand Down
54 changes: 28 additions & 26 deletions yarl/_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,32 +541,6 @@ def __truediv__(self, name: str) -> "URL":
def __mod__(self, query: Query) -> "URL":
return self.update_query(query)

def __sub__(self, other: object) -> "URL":
"""Return a new URL with a relative path between two other URL objects.
Note that both URLs must have the same scheme and netloc.
Example:
>>> target = URL("http://example.com/path/index.html")
>>> base = URL("http://example.com/")
>>> target - base
URL('path/index.html')
"""

if type(other) is not URL:
return NotImplemented

target_scheme, target_netloc, target_path, _, _ = self._val
base_scheme, base_netloc, base_path, _, _ = 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")

path = calculate_relative_path(target_path, base_path)
return self._from_parts("", "", path, "", "")

def __bool__(self) -> bool:
return bool(self._netloc or self._path or self._query or self._fragment)

Expand Down Expand Up @@ -1391,6 +1365,34 @@ def with_suffix(

return self.with_name(name, keep_query=keep_query, keep_fragment=keep_fragment)

def relative_to(self, other: object) -> "URL":
"""Return a new URL with a relative path between two other URL objects.
Note that both URLs must have the same scheme and netloc.
Example:
>>> target = URL("http://example.com/path/index.html")
>>> base = URL("http://example.com/")
>>> target.relative_to(base)
URL('path/index.html')
>>> base.relative_to(target)
URL('..')
"""

if type(other) is not URL:
return NotImplemented

target_scheme, target_netloc, target_path, _, _ = self._val
base_scheme, base_netloc, base_path, _, _ = 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")

path = calculate_relative_path(target_path, base_path)
return from_parts("", "", path, "", "")

def join(self, url: "URL") -> "URL":
"""Join URLs
Expand Down

0 comments on commit 4033cb7

Please sign in to comment.