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

Allow replacing path without replacing query or fragment #1421

Merged
merged 25 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
930554d
Add keep_query and keep_fragment arguments to with_path
paul-nameless Nov 18, 2024
f85a084
Add changelog entries
paul-nameless Nov 18, 2024
7335890
One line to make it more readable
paul-nameless Nov 18, 2024
aa07b97
Update changelog message
paul-nameless Nov 18, 2024
bd1d161
Update changelog message to pass lint
paul-nameless Nov 18, 2024
eca4ce6
Add test to cover arguments false. Rename flags
paul-nameless Nov 18, 2024
32a8cd2
Update 111.bugfix.rst
bdraco Nov 18, 2024
3bb511f
changelog
bdraco Nov 18, 2024
1eb087e
symlink
bdraco Nov 18, 2024
026d05d
Update documentation with new flags
paul-nameless Nov 18, 2024
9a9a45a
Add versionadded
paul-nameless Nov 18, 2024
fc373e4
Update docs/api.rst
paul-nameless Nov 18, 2024
59d1b92
Update docs/api.rst
bdraco Nov 19, 2024
1a61eee
Add flags to with_path and with_suffix
paul-nameless Nov 19, 2024
73501a4
Add documentation
paul-nameless Nov 19, 2024
e56e140
Update changelog
paul-nameless Nov 19, 2024
6b3fefe
Rewrite tests to use parametrize
paul-nameless Nov 19, 2024
f65530b
Update docs. Add required named params to method defition
paul-nameless Nov 20, 2024
822726f
Update tests
paul-nameless Nov 20, 2024
6e3cfc3
Update CHANGES/1421.feature.rst
paul-nameless Nov 20, 2024
83d9183
Merge branch 'master' into with-path-keep-query
bdraco Nov 20, 2024
53ae54d
Merge branch 'master' into with-path-keep-query
paul-nameless Nov 20, 2024
95d1e4e
Update docs/api.rst
paul-nameless Nov 21, 2024
439bfe8
Update docs/api.rst
paul-nameless Nov 21, 2024
41ecc98
Update docs/api.rst
paul-nameless Nov 21, 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
1 change: 1 addition & 0 deletions CHANGES/111.feature.rst
1 change: 1 addition & 0 deletions CHANGES/1421.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ``keep_query`` and ``keep_fragment`` flags in the :py:meth:`yarl.URL.with_path`, :py:meth:`yarl.URL.with_name` and :py:meth:`yarl.URL.with_suffix` methods, allowing users to optionally retain the query string and fragment in the resulting URL when replacing the path -- by :user:`paul-nameless`.
24 changes: 21 additions & 3 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -652,10 +652,16 @@ section generates a new :class:`URL` instance.
>>> URL('http://example.com:8888').with_port(None)
URL('http://example.com')

.. method:: URL.with_path(path)
.. method:: URL.with_path(path, *, keep_query=False, keep_fragment=False)

Return a new URL with *path* replaced, encode *path* if needed.

If ``keep_query=True`` or ``keep_fragment=True`` it retains the existing query or fragment in the URL.

.. versionchanged:: 1.18

Added *keep_query* and *keep_fragment* parameters.

.. doctest::
bdraco marked this conversation as resolved.
Show resolved Hide resolved

>>> URL('http://example.com/').with_path('/path/to')
Expand Down Expand Up @@ -857,27 +863,39 @@ section generates a new :class:`URL` instance.
>>> URL('http://example.com/path#frag').with_fragment(None)
URL('http://example.com/path')

.. method:: URL.with_name(name)
.. method:: URL.with_name(name, *, keep_query=False, keep_fragment=False)

Return a new URL with *name* (last part of *path*) replaced and
cleaned up *query* and *fragment* parts.

Name is encoded if needed.

If ``keep_query=True`` or ``keep_fragment=True`` it retains the existing query or fragment in the URL.

.. versionchanged:: 1.18

Added *keep_query* and *keep_fragment* parameters.

.. doctest::

>>> URL('http://example.com/path/to?arg#frag').with_name('new')
URL('http://example.com/path/new')
>>> URL('http://example.com/path/to').with_name("ім'я")
URL('http://example.com/path/%D1%96%D0%BC%27%D1%8F')

.. method:: URL.with_suffix(suffix)
.. method:: URL.with_suffix(suffix, *, keep_query=False, keep_fragment=False)

Return a new URL with *suffix* (file extension of *name*) replaced and
cleaned up *query* and *fragment* parts.

Name is encoded if needed.

If ``keep_query=True`` or ``keep_fragment=True`` it retains the existing query or fragment in the URL.

.. versionchanged:: 1.18

Added *keep_query* and *keep_fragment* parameters.

.. doctest::

>>> URL('http://example.com/path/to?arg#frag').with_suffix('.doc')
Expand Down
125 changes: 124 additions & 1 deletion tests/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f "
)
_VERTICAL_COLON = "\ufe13" # normalizes to ":"
_FULL_WITH_NUMBER_SIGN = "\uFF03" # normalizes to "#"
_FULL_WITH_NUMBER_SIGN = "\uff03" # normalizes to "#"
_ACCOUNT_OF = "\u2100" # normalizes to "a/c"


bdraco marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -1240,6 +1240,47 @@ def test_with_path_fragment():
assert str(url.with_path("/test")) == "http://example.com/test"


@pytest.mark.parametrize(
("original_url", "keep_query", "keep_fragment", "expected_url"),
[
pytest.param(
"http://example.com?a=b#frag",
True,
False,
"http://example.com/test?a=b",
id="query-only",
),
pytest.param(
"http://example.com?a=b#frag",
False,
True,
"http://example.com/test#frag",
id="fragment-only",
),
pytest.param(
"http://example.com?a=b#frag",
True,
True,
"http://example.com/test?a=b#frag",
id="all",
),
pytest.param(
"http://example.com?a=b#frag",
False,
False,
"http://example.com/test",
id="none",
),
],
)
def test_with_path_keep_query_keep_fragment_flags(
original_url, keep_query, keep_fragment, expected_url
):
url = URL(original_url)
url2 = url.with_path("/test", keep_query=keep_query, keep_fragment=keep_fragment)
assert str(url2) == expected_url

bdraco marked this conversation as resolved.
Show resolved Hide resolved

def test_with_path_empty():
url = URL("http://example.com/test")
assert str(url.with_path("")) == "http://example.com"
Expand Down Expand Up @@ -1319,6 +1360,47 @@ def test_with_name():
assert url2.path == "/a/c"


@pytest.mark.parametrize(
("original_url", "keep_query", "keep_fragment", "expected_url"),
[
pytest.param(
"http://example.com/path/to?a=b#frag",
True,
False,
"http://example.com/path/newname?a=b",
id="query-only",
),
pytest.param(
"http://example.com/path/to?a=b#frag",
False,
True,
"http://example.com/path/newname#frag",
id="fragment-only",
),
pytest.param(
"http://example.com/path/to?a=b#frag",
True,
True,
"http://example.com/path/newname?a=b#frag",
id="all",
),
pytest.param(
"http://example.com/path/to?a=b#frag",
False,
False,
"http://example.com/path/newname",
id="none",
),
],
)
def test_with_name_keep_query_keep_fragment_flags(
original_url, keep_query, keep_fragment, expected_url
):
url = URL(original_url)
url2 = url.with_name("newname", keep_query=keep_query, keep_fragment=keep_fragment)
assert str(url2) == expected_url


def test_with_name_for_naked_path():
url = URL("http://example.com")
url2 = url.with_name("a")
Expand Down Expand Up @@ -1409,6 +1491,47 @@ def test_with_suffix():
assert url2.path == "/a/b.c"


@pytest.mark.parametrize(
("original_url", "keep_query", "keep_fragment", "expected_url"),
[
pytest.param(
"http://example.com/path/to.txt?a=b#frag",
True,
False,
"http://example.com/path/to.md?a=b",
id="query-only",
),
pytest.param(
"http://example.com/path/to.txt?a=b#frag",
False,
True,
"http://example.com/path/to.md#frag",
id="fragment-only",
),
pytest.param(
"http://example.com/path/to.txt?a=b#frag",
True,
True,
"http://example.com/path/to.md?a=b#frag",
id="all",
),
pytest.param(
"http://example.com/path/to.txt?a=b#frag",
False,
False,
"http://example.com/path/to.md",
id="none",
),
],
)
def test_with_suffix_keep_query_keep_fragment_flags(
original_url, keep_query, keep_fragment, expected_url
):
url = URL(original_url)
url2 = url.with_suffix(".md", keep_query=keep_query, keep_fragment=keep_fragment)
assert str(url2) == expected_url


def test_with_suffix_for_naked_path():
url = URL("http://example.com")
with pytest.raises(ValueError) as excinfo:
Expand Down
37 changes: 31 additions & 6 deletions yarl/_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -1114,7 +1114,14 @@ def with_port(self, port: Union[int, None]) -> "URL":
self._scheme, netloc, self._path, self._query, self._fragment
)

def with_path(self, path: str, *, encoded: bool = False) -> "URL":
def with_path(
self,
path: str,
*,
encoded: bool = False,
keep_query: bool = False,
keep_fragment: bool = False,
) -> "URL":
"""Return a new URL with path replaced."""
netloc = self._netloc
if not encoded:
Expand All @@ -1123,7 +1130,9 @@ def with_path(self, path: str, *, encoded: bool = False) -> "URL":
path = normalize_path(path) if "." in path else path
if path and path[0] != "/":
path = f"/{path}"
return self._from_parts(self._scheme, netloc, path, "", "")
query = self._query if keep_query else ""
fragment = self._fragment if keep_fragment else ""
return self._from_parts(self._scheme, netloc, path, query, fragment)

@overload
def with_query(self, query: Query) -> "URL": ...
Expand Down Expand Up @@ -1271,7 +1280,13 @@ def with_fragment(self, fragment: Union[str, None]) -> "URL":
self._scheme, self._netloc, self._path, self._query, raw_fragment
)

def with_name(self, name: str) -> "URL":
def with_name(
self,
name: str,
*,
keep_query: bool = False,
keep_fragment: bool = False,
) -> "URL":
"""Return a new URL with name (last part of path) replaced.

Query and fragment parts are cleaned up.
Expand All @@ -1298,9 +1313,18 @@ def with_name(self, name: str) -> "URL":
parts[-1] = name
if parts[0] == "/":
parts[0] = "" # replace leading '/'
return self._from_parts(self._scheme, netloc, "/".join(parts), "", "")

def with_suffix(self, suffix: str) -> "URL":
query = self._query if keep_query else ""
fragment = self._fragment if keep_fragment else ""
return self._from_parts(self._scheme, netloc, "/".join(parts), query, fragment)

def with_suffix(
self,
suffix: str,
*,
keep_query: bool = False,
keep_fragment: bool = False,
) -> "URL":
"""Return a new URL with suffix (file extension of name) replaced.

Query and fragment parts are cleaned up.
Expand All @@ -1316,7 +1340,8 @@ def with_suffix(self, suffix: str) -> "URL":
raise ValueError(f"{self!r} has an empty name")
old_suffix = self.raw_suffix
name = name + suffix if not old_suffix else name[: -len(old_suffix)] + suffix
return self.with_name(name)

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

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