Skip to content

Commit

Permalink
Allow replacing path without replacing query or fragment (#1421)
Browse files Browse the repository at this point in the history
* Add keep_query and keep_fragment arguments to with_path

* Add changelog entries

* One line to make it more readable

* Update changelog message

* Update changelog message to pass lint

* Add test to cover arguments false. Rename flags

* Update 111.bugfix.rst

* changelog

* symlink

* Update documentation with new flags

* Add versionadded

* Update docs/api.rst

Co-authored-by: Andrew Svetlov <[email protected]>

* Update docs/api.rst

* Add flags to with_path and with_suffix

* Add documentation

* Update changelog

* Rewrite tests to use parametrize

* Update docs. Add required named params to method defition

* Update tests

* Update CHANGES/1421.feature.rst

Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>

* Update docs/api.rst

Co-authored-by: Andrew Svetlov <[email protected]>

* Update docs/api.rst

Co-authored-by: Andrew Svetlov <[email protected]>

* Update docs/api.rst

Co-authored-by: Andrew Svetlov <[email protected]>

---------

Co-authored-by: J. Nick Koston <[email protected]>
Co-authored-by: Andrew Svetlov <[email protected]>
Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
  • Loading branch information
4 people authored Nov 21, 2024
1 parent 2b94725 commit fc08cb8
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 10 deletions.
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::

>>> 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"


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


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

0 comments on commit fc08cb8

Please sign in to comment.