Skip to content
Draft
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
10 changes: 10 additions & 0 deletions CHANGES/1770.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Fixed :py:meth:`~yarl.URL.with_path` to preserve the bare-relative
status of a URL when the new path is given without a leading ``/``.
Previously ``URL("foo/bar").with_path("abs")`` silently returned
``URL("/abs")`` (an absolute-path reference), which changes how the
URL resolves against any base. The leading ``/`` is now only added
for URLs that have a netloc, so ``URL("foo/bar").with_path("abs")``
returns ``URL("abs")`` and ``URL("foo/bar").with_path("/abs")`` still
returns ``URL("/abs")``.

-- by :user:`HrachShah`.
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ macOS
mailto
manylinux
multi
netloc
nightlies
pre
pydantic
Expand Down
31 changes: 31 additions & 0 deletions tests/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,37 @@ def test_with_path_leading_slash() -> None:
assert url.with_path("test").path == "/test"


def test_with_path_preserves_bare_relative() -> None:
# Issue #1770: with_path must not silently promote a bare-relative
# URL (one without scheme/netloc) into an absolute-path reference
# when the new path does not start with "/".
url = URL("foo/bar")
new = url.with_path("abs")
assert str(new) == "abs"
assert new.raw_path == "abs"
assert not new.is_absolute()


def test_with_path_explicit_slash_preserved_on_bare_relative() -> None:
# The same URL, with an explicit "/abs" path, must still produce
# the leading slash.
url = URL("foo/bar")
new = url.with_path("/abs")
assert str(new) == "/abs"
assert new.raw_path == "/abs"


def test_with_path_no_netloc_join_round_trip() -> None:
# The original URL stays bare-relative across with_path, so the
# join round-trips through both directions without absorbing the
# leading-slash promotion from the previous behaviour.
url = URL("foo/bar")
new = url.with_path("abs")
assert new.join(URL("baz")) == URL("baz")
base = URL("http://example.com/x/y")
assert base.join(new) == URL("http://example.com/x/abs")


# with_fragment


Expand Down
2 changes: 1 addition & 1 deletion yarl/_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -1210,7 +1210,7 @@ def with_path(
path = PATH_QUOTER(path)
if netloc:
path = normalize_path(path) if "." in path else path
if path and path[0] != "/":
if path and path[0] != "/" and netloc:
path = f"/{path}"
query = self._query if keep_query else ""
fragment = self._fragment if keep_fragment else ""
Expand Down
Loading