Skip to content

Commit a38427f

Browse files
committed
Allow for modification of certain components
When using the URIBulider, it's plausible that one would want to use a base URI and then modify components like the path and query string without replacing them wholesale. Let's add two methods for that as well as a convenience method to replace the `.finalize().unsplit()` dance. This updates our docs to be a bit friendlier and adds release notes for this change. Closes #29
1 parent 3657ec4 commit a38427f

File tree

6 files changed

+243
-1
lines changed

6 files changed

+243
-1
lines changed

docs/source/api-ref/builder.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@
1616

1717
.. automethod:: rfc3986.builder.URIBuilder.add_path
1818

19+
.. automethod:: rfc3986.builder.URIBuilder.extend_path
20+
1921
.. automethod:: rfc3986.builder.URIBuilder.add_query_from
2022

23+
.. automethod:: rfc3986.builder.URIBuilder.extend_query_with
24+
2125
.. automethod:: rfc3986.builder.URIBuilder.add_query
2226

2327
.. automethod:: rfc3986.builder.URIBuilder.add_fragment
2428

2529
.. automethod:: rfc3986.builder.URIBuilder.finalize
30+
31+
.. automethod:: rfc3986.builder.URIBuilder.geturl

docs/source/release-notes/unreleased.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,17 @@
66

77
See also `GitHub #57`_
88

9+
- Add :meth:`~rfc3986.builder.URIBuilder.extend_path`,
10+
:meth:`~rfc3986.builder.URIBuilder.extend_query_with`,
11+
:meth:`~rfc3986.builder.URIBuilder.geturl` to
12+
:class:`~rfc3986.builder.URIBuilder`.
13+
14+
See also `GitHub #29`_
915

1016
.. links
1117
18+
.. _GitHub #29:
19+
https://github.com/python-hyper/rfc3986/issues/29
20+
1221
.. _GitHub #57:
1322
https://github.com/python-hyper/rfc3986/issues/57

docs/source/user/building.rst

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ Example Usage
2222
.. note::
2323

2424
All of the methods on a :class:`~rfc3986.builder.URIBuilder` are
25-
chainable (except :meth:`~rfc3986.builder.URIBuilder.finalize`).
25+
chainable (except :meth:`~rfc3986.builder.URIBuilder.finalize` and
26+
:meth:`~rfc3986.builder.URIBuilder.geturl` as neither returns a
27+
:class:`~rfc3986.builder.URIBuilder`).
28+
29+
Building From Scratch
30+
---------------------
2631

2732
Let's build a basic URL with just a scheme and host. First we create an
2833
instance of :class:`~rfc3986.builder.URIBuilder`. Then we call
@@ -42,6 +47,10 @@ a :class:`~rfc3986.uri.URIReference` and call
4247
... ).finalize().unsplit())
4348
https://github.com
4449

50+
51+
Replacing Components of a URI
52+
-----------------------------
53+
4554
It is possible to update an existing URI by constructing a builder from an
4655
instance of :class:`~rfc3986.uri.URIReference` or a textual representation:
4756

@@ -53,6 +62,9 @@ instance of :class:`~rfc3986.uri.URIReference` or a textual representation:
5362
... ).finalize().unsplit())
5463
https://github.com
5564

65+
The Builder is Immutable
66+
------------------------
67+
5668
Each time you invoke a method, you get a new instance of a
5769
:class:`~rfc3986.builder.URIBuilder` class so you can build several different
5870
URLs from one base instance.
@@ -74,6 +86,32 @@ URLs from one base instance.
7486
... ).finalize().unsplit())
7587
https://api.github.com/repos/sigmavirus24/rfc3986
7688

89+
Convenient Path Management
90+
--------------------------
91+
92+
Because our builder is immutable, one could use the
93+
:class:`~rfc3986.builder.URIBuilder` class to build a class to make HTTP
94+
Requests that used the provided path to extend the original one.
95+
96+
.. doctest::
97+
98+
>>> from rfc3986 import builder
99+
>>> github_builder = builder.URIBuilder().add_scheme(
100+
... 'https'
101+
... ).add_host(
102+
... 'api.github.com'
103+
... ).add_path(
104+
... '/users'
105+
... )
106+
>>> print(github_builder.extend_path("sigmavirus24").geturl())
107+
https://api.github.com/users/sigmavirus24
108+
>>> print(github_builder.extend_path("lukasa").geturl())
109+
https://api.github.com/users/lukasa
110+
111+
112+
Convenient Credential Handling
113+
------------------------------
114+
77115
|rfc3986| makes adding authentication credentials convenient. It takes care of
78116
making the credentials URL safe. There are some characters someone might want
79117
to include in a URL that are not safe for the authority component of a URL.
@@ -91,6 +129,9 @@ to include in a URL that are not safe for the authority component of a URL.
91129
... ).finalize().unsplit())
92130
https://us3r:p%[email protected]
93131

132+
Managing Query String Parameters
133+
--------------------------------
134+
94135
Further, |rfc3986| attempts to simplify the process of adding query parameters
95136
to a URL. For example, if we were using Elasticsearch, we might do something
96137
like:
@@ -109,6 +150,22 @@ like:
109150
... ).finalize().unsplit())
110151
https://search.example.com/_search?q=repo%3Asigmavirus24%2Frfc3986&sort=created_at%3Aasc
111152

153+
If one also had an existing URL with query string that we merely wanted to
154+
append to, we can also do that with |rfc3986|.
155+
156+
.. doctest::
157+
158+
>>> from rfc3986 import builder
159+
>>> print(builder.URIBuilder().from_uri(
160+
... 'https://search.example.com/_search?q=repo%3Asigmavirus24%2Frfc3986'
161+
... ).extend_query_with(
162+
... [('sort', 'created_at:asc')]
163+
... ).finalize().unsplit())
164+
https://search.example.com/_search?q=repo%3Asigmavirus24%2Frfc3986&sort=created_at%3Aasc
165+
166+
Adding Fragments
167+
----------------
168+
112169
Finally, we provide a way to add a fragment to a URL. Let's build up a URL to
113170
view the section of the RFC that refers to fragments:
114171

src/rfc3986/builder.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,35 @@ def add_path(self, path):
234234
fragment=self.fragment,
235235
)
236236

237+
def extend_path(self, path):
238+
"""Extend the existing path value with the provided value.
239+
240+
.. versionadded:: 1.5.0
241+
242+
.. code-block:: python
243+
244+
>>> URIBuilder(path="/users").extend_path("/sigmavirus24")
245+
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
246+
path='/users/sigmavirus24', query=None, fragment=None)
247+
248+
>>> URIBuilder(path="/users/").extend_path("/sigmavirus24")
249+
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
250+
path='/users/sigmavirus24', query=None, fragment=None)
251+
252+
>>> URIBuilder(path="/users/").extend_path("sigmavirus24")
253+
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
254+
path='/users/sigmavirus24', query=None, fragment=None)
255+
256+
>>> URIBuilder(path="/users").extend_path("sigmavirus24")
257+
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
258+
path='/users/sigmavirus24', query=None, fragment=None)
259+
260+
"""
261+
existing_path = self.path or ""
262+
path = "{}/{}".format(existing_path.rstrip("/"), path.lstrip("/"))
263+
264+
return self.add_path(path)
265+
237266
def add_query_from(self, query_items):
238267
"""Generate and add a query a dictionary or list of tuples.
239268
@@ -260,6 +289,27 @@ def add_query_from(self, query_items):
260289
fragment=self.fragment,
261290
)
262291

292+
def extend_query_with(self, query_items):
293+
"""Extend the existing query string with the new query items.
294+
295+
.. versionadded:: 1.5.0
296+
297+
.. code-block:: python
298+
299+
>>> URIBuilder(query='a=b+c').extend_query_with({'a': 'b c'})
300+
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
301+
path=None, query='a=b+c&a=b+c', fragment=None)
302+
303+
>>> URIBuilder(query='a=b+c').extend_query_with([('a', 'b c')])
304+
URIBuilder(scheme=None, userinfo=None, host=None, port=None,
305+
path=None, query='a=b+c&a=b+c', fragment=None)
306+
"""
307+
original_query_items = compat.parse_qsl(self.query or "")
308+
if not isinstance(query_items, list):
309+
query_items = list(query_items.items())
310+
311+
return self.add_query_from(original_query_items + query_items)
312+
263313
def add_query(self, query):
264314
"""Add a pre-formated query string to the URI.
265315
@@ -324,3 +374,13 @@ def finalize(self):
324374
self.query,
325375
self.fragment,
326376
)
377+
378+
def geturl(self):
379+
"""Generate the URL from this builder.
380+
381+
.. versionadded:: 1.5.0
382+
383+
This is an alternative to calling :meth:`finalize` and keeping the
384+
:class:`rfc3986.uri.URIReference` around.
385+
"""
386+
return self.finalize().unsplit()

src/rfc3986/compat.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
except ImportError: # Python 2.x
2121
from urllib import quote as urlquote
2222

23+
try:
24+
from urllib.parse import parse_qsl
25+
except ImportError: # Python 2.x
26+
from urlparse import parse_qsl
27+
2328
try:
2429
from urllib.parse import urlencode
2530
except ImportError: # Python 2.x
@@ -30,6 +35,7 @@
3035
"to_str",
3136
"urlquote",
3237
"urlencode",
38+
"parse_qsl",
3339
)
3440

3541
PY3 = (3, 0) <= sys.version_info < (4, 0)

tests/test_builder.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
"""Module containing the tests for the URIBuilder object."""
16+
try:
17+
from urllib.parse import parse_qs
18+
except ImportError:
19+
from urlparse import parse_qs
20+
1621
import pytest
1722

1823
from rfc3986 import builder, uri_reference
@@ -191,6 +196,88 @@ def test_add_fragment():
191196
assert uribuilder.fragment == "section-2.5.1"
192197

193198

199+
@pytest.mark.parametrize(
200+
"uri, extend_with, expected_path",
201+
[
202+
("https://api.github.com", "/users", "/users"),
203+
("https://api.github.com", "/users/", "/users/"),
204+
("https://api.github.com", "users", "/users"),
205+
("https://api.github.com", "users/", "/users/"),
206+
("", "users/", "/users/"),
207+
("", "users", "/users"),
208+
("?foo=bar", "users", "/users"),
209+
(
210+
"https://api.github.com/users/",
211+
"sigmavirus24",
212+
"/users/sigmavirus24",
213+
),
214+
(
215+
"https://api.github.com/users",
216+
"sigmavirus24",
217+
"/users/sigmavirus24",
218+
),
219+
(
220+
"https://api.github.com/users",
221+
"/sigmavirus24",
222+
"/users/sigmavirus24",
223+
),
224+
],
225+
)
226+
def test_extend_path(uri, extend_with, expected_path):
227+
"""Verify the behaviour of extend_path."""
228+
uribuilder = (
229+
builder.URIBuilder()
230+
.from_uri(uri_reference(uri))
231+
.extend_path(extend_with)
232+
)
233+
assert uribuilder.path == expected_path
234+
235+
236+
@pytest.mark.parametrize(
237+
"uri, extend_with, expected_query",
238+
[
239+
(
240+
"https://github.com",
241+
[("a", "b c"), ("d", "e&f")],
242+
{"a": ["b c"], "d": ["e&f"]},
243+
),
244+
(
245+
"https://github.com?a=0",
246+
[("a", "b c"), ("d", "e&f")],
247+
{"a": ["0", "b c"], "d": ["e&f"]},
248+
),
249+
(
250+
"https://github.com?a=0&e=f",
251+
[("a", "b c"), ("d", "e&f")],
252+
{"a": ["0", "b c"], "e": ["f"], "d": ["e&f"]},
253+
),
254+
(
255+
"https://github.com",
256+
{"a": "b c", "d": "e&f"},
257+
{"a": ["b c"], "d": ["e&f"]},
258+
),
259+
(
260+
"https://github.com?a=0",
261+
{"a": "b c", "d": "e&f"},
262+
{"a": ["0", "b c"], "d": ["e&f"]},
263+
),
264+
(
265+
"https://github.com?a=0&e=f",
266+
{"a": "b c", "d": "e&f"},
267+
{"a": ["0", "b c"], "e": ["f"], "d": ["e&f"]},
268+
),
269+
],
270+
)
271+
def test_extend_query_with(uri, extend_with, expected_query):
272+
"""Verify the behaviour of extend_query_with."""
273+
uribuilder = (
274+
builder.URIBuilder()
275+
.from_uri(uri_reference(uri))
276+
.extend_query_with(extend_with)
277+
)
278+
assert parse_qs(uribuilder.query) == expected_query
279+
280+
194281
def test_finalize():
195282
"""Verify the whole thing."""
196283
uri = (
@@ -207,3 +294,20 @@ def test_finalize():
207294
"sigmavirus24/rfc3986"
208295
)
209296
assert expected == uri
297+
298+
299+
def test_geturl():
300+
"""Verify the short-cut to the URL."""
301+
uri = (
302+
builder.URIBuilder()
303+
.add_scheme("https")
304+
.add_credentials("sigmavirus24", "not-my-re@l-password")
305+
.add_host("github.com")
306+
.add_path("sigmavirus24/rfc3986")
307+
.geturl()
308+
)
309+
expected = (
310+
"https://sigmavirus24:not-my-re%[email protected]/"
311+
"sigmavirus24/rfc3986"
312+
)
313+
assert expected == uri

0 commit comments

Comments
 (0)