Skip to content

Commit 8099a52

Browse files
authored
Merge branch 'master' into excuse-my-french
2 parents 85a6101 + d261a3a commit 8099a52

File tree

6 files changed

+47
-12
lines changed

6 files changed

+47
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## dev (not yet released)
44

5+
* Also accept dictionaries as ‘query=’ arguments (see #50)
6+
57
## 17.3.1
68

79
*(August 19, 2017)*

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ Then, hyperlink away!
3232
```python
3333
from hyperlink import URL
3434

35-
url = URL.from_text('http://github.com/mahmoud/hyperlink?utm_source=README')
36-
utm_source = url.get('utm_source')
37-
better_url = url.replace(scheme='https')
38-
user_url = better_url.click('..')
35+
url = URL.from_text(u'http://github.com/mahmoud/hyperlink?utm_source=README')
36+
utm_source = url.get(u'utm_source')
37+
better_url = url.replace(scheme=u'https')
38+
user_url = better_url.click(u'..')
3939
```
4040

4141
See the full API docs on [Read the Docs][docs].

docs/design.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ by the query parameters, sometimes called "query arguments" or "GET
9292
parameters". Regardless of what you call them, they are encoded in
9393
the query string portion of the URL, and they are very powerful.
9494

95+
In the simplest case, these query parameters can be provided as a
96+
dictionary:
97+
98+
>>> url = URL.from_text('http://example.com/')
99+
>>> url = url.replace(query={'a': 'b', 'c': 'd'})
100+
>>> url.to_text()
101+
u'http://example.com/?a=b&c=d'
102+
95103
Query parameters are actually a type of "multidict", where a given key
96104
can have multiple values. This is why the :meth:`~URL.get()` method
97105
returns a list of strings. Keys can also have no value, which is

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ hyperlink
1010
URLs. Based on `RFC 3986`_ and `RFC 3987`_, the Hyperlink URL balances
1111
simplicity and correctness for both :ref:`URIs and IRIs <uris_and_iris>`.
1212

13-
Hyperlink is tested against Python 2.7, 3.4, 3.5, and PyPy.
13+
Hyperlink is tested against Python 2.7, 3.4, 3.5, 3.6, and PyPy.
1414

1515
For an introduction to the hyperlink library, its background, and URLs
1616
in general, see `this talk from PyConWeb 2017`_ (and `the accompanying

hyperlink/_url.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616
"""
1717

1818
import re
19+
import sys
1920
import string
2021
import socket
2122
from unicodedata import normalize
23+
24+
try:
25+
from collections.abc import Mapping
26+
except ImportError: # Python 2
27+
from collections import Mapping
2228
try:
2329
from socket import inet_pton
2430
except ImportError:
@@ -52,6 +58,7 @@ def inet_pton(address_family, ip_string):
5258
raise socket.error('unknown address family')
5359

5460

61+
PY2 = (sys.version_info[0] == 2)
5562
unicode = type(u'')
5663
try:
5764
unichr
@@ -425,7 +432,7 @@ def _textcheck(name, value, delims=frozenset(), nullable=False):
425432
if nullable and value is None:
426433
return value # used by query string values
427434
else:
428-
str_name = "unicode" if bytes is str else "str"
435+
str_name = "unicode" if PY2 else "str"
429436
exp = str_name + ' or NoneType' if nullable else str_name
430437
raise TypeError('expected %s for %s, got %r' % (exp, name, value))
431438
if delims and set(value) & set(delims): # TODO: test caching into regexes
@@ -434,6 +441,19 @@ def _textcheck(name, value, delims=frozenset(), nullable=False):
434441
return value
435442

436443

444+
def iter_pairs(iterable):
445+
"""
446+
Iterate over the (key, value) pairs in ``iterable``.
447+
448+
This handles dictionaries sensibly, and falls back to assuming the
449+
iterable yields (key, value) pairs. This behaviour is similar to
450+
what Python's ``dict()`` constructor does.
451+
"""
452+
if isinstance(iterable, Mapping):
453+
iterable = iterable.items()
454+
return iter(iterable)
455+
456+
437457
def _decode_unreserved(text, normalize_case=False):
438458
return _percent_decode(text, normalize_case=normalize_case,
439459
_decode_map=_UNRESERVED_DECODE_MAP)
@@ -639,8 +659,8 @@ class URL(object):
639659
for more info.
640660
path (tuple): A tuple of strings representing the
641661
slash-separated parts of the path.
642-
query (tuple): The query parameters, as a tuple of
643-
key-value pairs.
662+
query (tuple): The query parameters, as a dictionary or
663+
as an iterable of key-value pairs.
644664
fragment (unicode): The fragment part of the URL.
645665
rooted (bool): Whether or not the path begins with a slash.
646666
userinfo (unicode): The username or colon-separated
@@ -695,8 +715,7 @@ def __init__(self, scheme=None, host=None, path=(), query=(), fragment=u'',
695715
self._query = tuple(
696716
(_textcheck("query parameter name", k, '&=#'),
697717
_textcheck("query parameter value", v, '&#', nullable=True))
698-
for (k, v) in query
699-
)
718+
for k, v in iter_pairs(query))
700719
self._fragment = _textcheck("fragment", fragment)
701720
self._port = _typecheck("port", port, int, NoneType)
702721
self._rooted = _typecheck("rooted", rooted, bool)
@@ -912,6 +931,8 @@ def replace(self, scheme=_UNSET, host=_UNSET, path=_UNSET, query=_UNSET,
912931
slash-separated parts of the path.
913932
query (tuple): The query parameters, as a tuple of
914933
key-value pairs.
934+
query (tuple): The query parameters, as a dictionary or
935+
as an iterable of key-value pairs.
915936
fragment (unicode): The fragment part of the URL.
916937
rooted (bool): Whether or not the path begins with a slash.
917938
userinfo (unicode): The username or colon-separated

hyperlink/test/test_url.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,10 +742,14 @@ def test_mailto(self):
742742
def test_queryIterable(self):
743743
"""
744744
When a L{URL} is created with a C{query} argument, the C{query}
745-
argument is converted into an N-tuple of 2-tuples.
745+
argument is converted into an N-tuple of 2-tuples, sensibly
746+
handling dictionaries.
746747
"""
748+
expected = (('alpha', 'beta'),)
747749
url = URL(query=[['alpha', 'beta']])
748-
self.assertEqual(url.query, (('alpha', 'beta'),))
750+
self.assertEqual(url.query, expected)
751+
url = URL(query={'alpha': 'beta'})
752+
self.assertEqual(url.query, expected)
749753

750754
def test_pathIterable(self):
751755
"""

0 commit comments

Comments
 (0)