@@ -467,9 +467,11 @@ def _encode_userinfo_part(text, maximal=True):
467
467
)
468
468
# As of Mar 11, 2017, there were 44 netloc schemes, and 13 non-netloc
469
469
470
+ NO_QUERY_PLUS_SCHEMES = set ()
470
471
471
- def register_scheme (text , uses_netloc = True , default_port = None ):
472
- # type: (Text, bool, Optional[int]) -> None
472
+
473
+ def register_scheme (text , uses_netloc = True , default_port = None , query_plus_is_space = True ):
474
+ # type: (Text, bool, Optional[int], bool) -> None
473
475
"""Registers new scheme information, resulting in correct port and
474
476
slash behavior from the URL object. There are dozens of standard
475
477
schemes preregistered, so this function is mostly meant for
@@ -485,6 +487,8 @@ def register_scheme(text, uses_netloc=True, default_port=None):
485
487
not. Defaults to True.
486
488
default_port: The default port, if any, for
487
489
netloc-using schemes.
490
+ query_plus_is_space: If true, a "+" in the query string should be
491
+ decoded as a space by DecodedURL.
488
492
489
493
.. _file an issue: https://github.com/mahmoud/hyperlink/issues
490
494
"""
@@ -510,6 +514,9 @@ def register_scheme(text, uses_netloc=True, default_port=None):
510
514
else :
511
515
raise ValueError ("uses_netloc expected bool, not: %r" % uses_netloc )
512
516
517
+ if not query_plus_is_space :
518
+ NO_QUERY_PLUS_SCHEMES .add (text )
519
+
513
520
return
514
521
515
522
@@ -1998,6 +2005,9 @@ class DecodedURL(object):
1998
2005
lazy: Set to True to avoid pre-decode all parts of the URL to check for
1999
2006
validity.
2000
2007
Defaults to False.
2008
+ query_plus_is_space: + characters in the query string should be treated
2009
+ as spaces when decoding. If unspecified, the default is taken from
2010
+ the scheme.
2001
2011
2002
2012
.. note::
2003
2013
@@ -2012,17 +2022,20 @@ class DecodedURL(object):
2012
2022
.. versionadded:: 18.0.0
2013
2023
"""
2014
2024
2015
- def __init__ (self , url = _EMPTY_URL , lazy = False ):
2016
- # type: (URL, bool) -> None
2025
+ def __init__ (self , url = _EMPTY_URL , lazy = False , query_plus_is_space = None ):
2026
+ # type: (URL, bool, Optional[bool] ) -> None
2017
2027
self ._url = url
2028
+ if query_plus_is_space is None :
2029
+ query_plus_is_space = url .scheme not in NO_QUERY_PLUS_SCHEMES
2030
+ self ._query_plus_is_space = query_plus_is_space
2018
2031
if not lazy :
2019
2032
# cache the following, while triggering any decoding
2020
2033
# issues with decodable fields
2021
2034
self .host , self .userinfo , self .path , self .query , self .fragment
2022
2035
return
2023
2036
2024
2037
@classmethod
2025
- def from_text (cls , text , lazy = False ):
2038
+ def from_text (cls , text , lazy = False , query_plus_is_space = None ):
2026
2039
# type: (Text, bool) -> DecodedURL
2027
2040
"""\
2028
2041
Make a `DecodedURL` instance from any text string containing a URL.
@@ -2034,7 +2047,7 @@ def from_text(cls, text, lazy=False):
2034
2047
Defaults to True.
2035
2048
"""
2036
2049
_url = URL .from_text (text )
2037
- return cls (_url , lazy = lazy )
2050
+ return cls (_url , lazy = lazy , query_plus_is_space = query_plus_is_space )
2038
2051
2039
2052
@property
2040
2053
def encoded_url (self ):
@@ -2059,22 +2072,34 @@ def to_iri(self):
2059
2072
"Passthrough to :meth:`~hyperlink.URL.to_iri()`"
2060
2073
return self ._url .to_iri ()
2061
2074
2075
+ def _clone (self , url ):
2076
+ # type: (URL) -> DecodedURL
2077
+ return self .__class__ (
2078
+ url ,
2079
+ # TODO: propagate laziness?
2080
+ query_plus_is_space = self ._query_plus_is_space ,
2081
+ )
2082
+
2062
2083
def click (self , href = u"" ):
2063
2084
# type: (Union[Text, URL, DecodedURL]) -> DecodedURL
2064
2085
"""Return a new DecodedURL wrapping the result of
2065
2086
:meth:`~hyperlink.URL.click()`
2066
2087
"""
2067
2088
if isinstance (href , DecodedURL ):
2068
2089
href = href ._url
2069
- return self .__class__ (self ._url .click (href = href ))
2090
+ return self ._clone (
2091
+ self ._url .click (href = href ),
2092
+ )
2070
2093
2071
2094
def sibling (self , segment ):
2072
2095
# type: (Text) -> DecodedURL
2073
2096
"""Automatically encode any reserved characters in *segment* and
2074
2097
return a new `DecodedURL` wrapping the result of
2075
2098
:meth:`~hyperlink.URL.sibling()`
2076
2099
"""
2077
- return self .__class__ (self ._url .sibling (_encode_reserved (segment )))
2100
+ return self ._clone (
2101
+ self ._url .sibling (_encode_reserved (segment )),
2102
+ )
2078
2103
2079
2104
def child (self , * segments ):
2080
2105
# type: (Text) -> DecodedURL
@@ -2085,7 +2110,7 @@ def child(self, *segments):
2085
2110
if not segments :
2086
2111
return self
2087
2112
new_segs = [_encode_reserved (s ) for s in segments ]
2088
- return self .__class__ (self ._url .child (* new_segs ))
2113
+ return self ._clone (self ._url .child (* new_segs ))
2089
2114
2090
2115
def normalize (
2091
2116
self ,
@@ -2101,7 +2126,7 @@ def normalize(
2101
2126
"""Return a new `DecodedURL` wrapping the result of
2102
2127
:meth:`~hyperlink.URL.normalize()`
2103
2128
"""
2104
- return self .__class__ (
2129
+ return self ._clone (
2105
2130
self ._url .normalize (
2106
2131
scheme , host , path , query , fragment , userinfo , percents
2107
2132
)
@@ -2148,11 +2173,16 @@ def path(self):
2148
2173
def query (self ):
2149
2174
# type: () -> QueryPairs
2150
2175
if not hasattr (self , "_query" ):
2176
+ if self ._query_plus_is_space :
2177
+ predecode = lambda x : x .replace ("+" , "%20" )
2178
+ else :
2179
+ predecode = lambda x : x
2180
+
2151
2181
self ._query = cast (
2152
2182
QueryPairs ,
2153
2183
tuple (
2154
2184
tuple (
2155
- _percent_decode (x , raise_subencoding_exc = True )
2185
+ _percent_decode (predecode ( x ) , raise_subencoding_exc = True )
2156
2186
if x is not None
2157
2187
else None
2158
2188
for x in (k , v )
@@ -2248,7 +2278,7 @@ def replace(
2248
2278
userinfo = userinfo_text ,
2249
2279
uses_netloc = uses_netloc ,
2250
2280
)
2251
- return self .__class__ (url = new_url )
2281
+ return self ._clone (url = new_url )
2252
2282
2253
2283
def get (self , name ):
2254
2284
# type: (Text) -> List[Optional[Text]]
0 commit comments