Skip to content

Commit fc08cb8

Browse files
paul-namelessbdracoasvetlovwebknjaz
authored
Allow replacing path without replacing query or fragment (#1421)
* 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]>
1 parent 2b94725 commit fc08cb8

File tree

5 files changed

+178
-10
lines changed

5 files changed

+178
-10
lines changed

CHANGES/111.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1421.feature.rst

CHANGES/1421.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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`.

docs/api.rst

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -652,10 +652,16 @@ section generates a new :class:`URL` instance.
652652
>>> URL('http://example.com:8888').with_port(None)
653653
URL('http://example.com')
654654

655-
.. method:: URL.with_path(path)
655+
.. method:: URL.with_path(path, *, keep_query=False, keep_fragment=False)
656656

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

659+
If ``keep_query=True`` or ``keep_fragment=True`` it retains the existing query or fragment in the URL.
660+
661+
.. versionchanged:: 1.18
662+
663+
Added *keep_query* and *keep_fragment* parameters.
664+
659665
.. doctest::
660666

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

860-
.. method:: URL.with_name(name)
866+
.. method:: URL.with_name(name, *, keep_query=False, keep_fragment=False)
861867

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

865871
Name is encoded if needed.
866872

873+
If ``keep_query=True`` or ``keep_fragment=True`` it retains the existing query or fragment in the URL.
874+
875+
.. versionchanged:: 1.18
876+
877+
Added *keep_query* and *keep_fragment* parameters.
878+
867879
.. doctest::
868880

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

874-
.. method:: URL.with_suffix(suffix)
886+
.. method:: URL.with_suffix(suffix, *, keep_query=False, keep_fragment=False)
875887

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

879891
Name is encoded if needed.
880892

893+
If ``keep_query=True`` or ``keep_fragment=True`` it retains the existing query or fragment in the URL.
894+
895+
.. versionchanged:: 1.18
896+
897+
Added *keep_query* and *keep_fragment* parameters.
898+
881899
.. doctest::
882900

883901
>>> URL('http://example.com/path/to?arg#frag').with_suffix('.doc')

tests/test_url.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f "
1111
)
1212
_VERTICAL_COLON = "\ufe13" # normalizes to ":"
13-
_FULL_WITH_NUMBER_SIGN = "\uFF03" # normalizes to "#"
13+
_FULL_WITH_NUMBER_SIGN = "\uff03" # normalizes to "#"
1414
_ACCOUNT_OF = "\u2100" # normalizes to "a/c"
1515

1616

@@ -1240,6 +1240,47 @@ def test_with_path_fragment():
12401240
assert str(url.with_path("/test")) == "http://example.com/test"
12411241

12421242

1243+
@pytest.mark.parametrize(
1244+
("original_url", "keep_query", "keep_fragment", "expected_url"),
1245+
[
1246+
pytest.param(
1247+
"http://example.com?a=b#frag",
1248+
True,
1249+
False,
1250+
"http://example.com/test?a=b",
1251+
id="query-only",
1252+
),
1253+
pytest.param(
1254+
"http://example.com?a=b#frag",
1255+
False,
1256+
True,
1257+
"http://example.com/test#frag",
1258+
id="fragment-only",
1259+
),
1260+
pytest.param(
1261+
"http://example.com?a=b#frag",
1262+
True,
1263+
True,
1264+
"http://example.com/test?a=b#frag",
1265+
id="all",
1266+
),
1267+
pytest.param(
1268+
"http://example.com?a=b#frag",
1269+
False,
1270+
False,
1271+
"http://example.com/test",
1272+
id="none",
1273+
),
1274+
],
1275+
)
1276+
def test_with_path_keep_query_keep_fragment_flags(
1277+
original_url, keep_query, keep_fragment, expected_url
1278+
):
1279+
url = URL(original_url)
1280+
url2 = url.with_path("/test", keep_query=keep_query, keep_fragment=keep_fragment)
1281+
assert str(url2) == expected_url
1282+
1283+
12431284
def test_with_path_empty():
12441285
url = URL("http://example.com/test")
12451286
assert str(url.with_path("")) == "http://example.com"
@@ -1319,6 +1360,47 @@ def test_with_name():
13191360
assert url2.path == "/a/c"
13201361

13211362

1363+
@pytest.mark.parametrize(
1364+
("original_url", "keep_query", "keep_fragment", "expected_url"),
1365+
[
1366+
pytest.param(
1367+
"http://example.com/path/to?a=b#frag",
1368+
True,
1369+
False,
1370+
"http://example.com/path/newname?a=b",
1371+
id="query-only",
1372+
),
1373+
pytest.param(
1374+
"http://example.com/path/to?a=b#frag",
1375+
False,
1376+
True,
1377+
"http://example.com/path/newname#frag",
1378+
id="fragment-only",
1379+
),
1380+
pytest.param(
1381+
"http://example.com/path/to?a=b#frag",
1382+
True,
1383+
True,
1384+
"http://example.com/path/newname?a=b#frag",
1385+
id="all",
1386+
),
1387+
pytest.param(
1388+
"http://example.com/path/to?a=b#frag",
1389+
False,
1390+
False,
1391+
"http://example.com/path/newname",
1392+
id="none",
1393+
),
1394+
],
1395+
)
1396+
def test_with_name_keep_query_keep_fragment_flags(
1397+
original_url, keep_query, keep_fragment, expected_url
1398+
):
1399+
url = URL(original_url)
1400+
url2 = url.with_name("newname", keep_query=keep_query, keep_fragment=keep_fragment)
1401+
assert str(url2) == expected_url
1402+
1403+
13221404
def test_with_name_for_naked_path():
13231405
url = URL("http://example.com")
13241406
url2 = url.with_name("a")
@@ -1409,6 +1491,47 @@ def test_with_suffix():
14091491
assert url2.path == "/a/b.c"
14101492

14111493

1494+
@pytest.mark.parametrize(
1495+
("original_url", "keep_query", "keep_fragment", "expected_url"),
1496+
[
1497+
pytest.param(
1498+
"http://example.com/path/to.txt?a=b#frag",
1499+
True,
1500+
False,
1501+
"http://example.com/path/to.md?a=b",
1502+
id="query-only",
1503+
),
1504+
pytest.param(
1505+
"http://example.com/path/to.txt?a=b#frag",
1506+
False,
1507+
True,
1508+
"http://example.com/path/to.md#frag",
1509+
id="fragment-only",
1510+
),
1511+
pytest.param(
1512+
"http://example.com/path/to.txt?a=b#frag",
1513+
True,
1514+
True,
1515+
"http://example.com/path/to.md?a=b#frag",
1516+
id="all",
1517+
),
1518+
pytest.param(
1519+
"http://example.com/path/to.txt?a=b#frag",
1520+
False,
1521+
False,
1522+
"http://example.com/path/to.md",
1523+
id="none",
1524+
),
1525+
],
1526+
)
1527+
def test_with_suffix_keep_query_keep_fragment_flags(
1528+
original_url, keep_query, keep_fragment, expected_url
1529+
):
1530+
url = URL(original_url)
1531+
url2 = url.with_suffix(".md", keep_query=keep_query, keep_fragment=keep_fragment)
1532+
assert str(url2) == expected_url
1533+
1534+
14121535
def test_with_suffix_for_naked_path():
14131536
url = URL("http://example.com")
14141537
with pytest.raises(ValueError) as excinfo:

yarl/_url.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,7 +1114,14 @@ def with_port(self, port: Union[int, None]) -> "URL":
11141114
self._scheme, netloc, self._path, self._query, self._fragment
11151115
)
11161116

1117-
def with_path(self, path: str, *, encoded: bool = False) -> "URL":
1117+
def with_path(
1118+
self,
1119+
path: str,
1120+
*,
1121+
encoded: bool = False,
1122+
keep_query: bool = False,
1123+
keep_fragment: bool = False,
1124+
) -> "URL":
11181125
"""Return a new URL with path replaced."""
11191126
netloc = self._netloc
11201127
if not encoded:
@@ -1123,7 +1130,9 @@ def with_path(self, path: str, *, encoded: bool = False) -> "URL":
11231130
path = normalize_path(path) if "." in path else path
11241131
if path and path[0] != "/":
11251132
path = f"/{path}"
1126-
return self._from_parts(self._scheme, netloc, path, "", "")
1133+
query = self._query if keep_query else ""
1134+
fragment = self._fragment if keep_fragment else ""
1135+
return self._from_parts(self._scheme, netloc, path, query, fragment)
11271136

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

1274-
def with_name(self, name: str) -> "URL":
1283+
def with_name(
1284+
self,
1285+
name: str,
1286+
*,
1287+
keep_query: bool = False,
1288+
keep_fragment: bool = False,
1289+
) -> "URL":
12751290
"""Return a new URL with name (last part of path) replaced.
12761291
12771292
Query and fragment parts are cleaned up.
@@ -1298,9 +1313,18 @@ def with_name(self, name: str) -> "URL":
12981313
parts[-1] = name
12991314
if parts[0] == "/":
13001315
parts[0] = "" # replace leading '/'
1301-
return self._from_parts(self._scheme, netloc, "/".join(parts), "", "")
13021316

1303-
def with_suffix(self, suffix: str) -> "URL":
1317+
query = self._query if keep_query else ""
1318+
fragment = self._fragment if keep_fragment else ""
1319+
return self._from_parts(self._scheme, netloc, "/".join(parts), query, fragment)
1320+
1321+
def with_suffix(
1322+
self,
1323+
suffix: str,
1324+
*,
1325+
keep_query: bool = False,
1326+
keep_fragment: bool = False,
1327+
) -> "URL":
13041328
"""Return a new URL with suffix (file extension of name) replaced.
13051329
13061330
Query and fragment parts are cleaned up.
@@ -1316,7 +1340,8 @@ def with_suffix(self, suffix: str) -> "URL":
13161340
raise ValueError(f"{self!r} has an empty name")
13171341
old_suffix = self.raw_suffix
13181342
name = name + suffix if not old_suffix else name[: -len(old_suffix)] + suffix
1319-
return self.with_name(name)
1343+
1344+
return self.with_name(name, keep_query=keep_query, keep_fragment=keep_fragment)
13201345

13211346
def join(self, url: "URL") -> "URL":
13221347
"""Join URLs

0 commit comments

Comments
 (0)