diff --git a/CHANGES/1392.feature.rst b/CHANGES/1392.feature.rst index 9876e4c0..a3cd4170 100644 --- a/CHANGES/1392.feature.rst +++ b/CHANGES/1392.feature.rst @@ -1,4 +1,4 @@ -Added support for using the :meth:`subtraction operator ` +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: @@ -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` diff --git a/docs/api.rst b/docs/api.rst index d0850b8c..c4f326a9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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. @@ -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 diff --git a/tests/test_url.py b/tests/test_url.py index 83be5baa..9ded0ecc 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -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 @@ -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(): diff --git a/tests/test_url_benchmarks.py b/tests/test_url_benchmarks.py index d0a247e8..225ac3e4 100644 --- a/tests/test_url_benchmarks.py +++ b/tests/test_url_benchmarks.py @@ -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: diff --git a/yarl/_url.py b/yarl/_url.py index 1b4a7ef9..3dbca979 100644 --- a/yarl/_url.py +++ b/yarl/_url.py @@ -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) @@ -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