_url.py 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291
  1. # -*- coding: utf-8 -*-
  2. u"""Hyperlink provides Pythonic URL parsing, construction, and rendering.
  3. Usage is straightforward::
  4. >>> from hyperlink import URL
  5. >>> url = URL.from_text('http://github.com/mahmoud/hyperlink?utm_source=docs')
  6. >>> url.host
  7. u'github.com'
  8. >>> secure_url = url.replace(scheme=u'https')
  9. >>> secure_url.get('utm_source')[0]
  10. u'docs'
  11. As seen here, the API revolves around the lightweight and immutable
  12. :class:`URL` type, documented below.
  13. """
  14. import re
  15. import string
  16. import socket
  17. try:
  18. from socket import inet_pton
  19. except ImportError:
  20. # based on https://gist.github.com/nnemkin/4966028
  21. # this code only applies on Windows Python 2.7
  22. import ctypes
  23. class _sockaddr(ctypes.Structure):
  24. _fields_ = [("sa_family", ctypes.c_short),
  25. ("__pad1", ctypes.c_ushort),
  26. ("ipv4_addr", ctypes.c_byte * 4),
  27. ("ipv6_addr", ctypes.c_byte * 16),
  28. ("__pad2", ctypes.c_ulong)]
  29. WSAStringToAddressA = ctypes.windll.ws2_32.WSAStringToAddressA
  30. WSAAddressToStringA = ctypes.windll.ws2_32.WSAAddressToStringA
  31. def inet_pton(address_family, ip_string):
  32. addr = _sockaddr()
  33. ip_string = ip_string.encode('ascii')
  34. addr.sa_family = address_family
  35. addr_size = ctypes.c_int(ctypes.sizeof(addr))
  36. if WSAStringToAddressA(ip_string, address_family, None, ctypes.byref(addr), ctypes.byref(addr_size)) != 0:
  37. raise socket.error(ctypes.FormatError())
  38. if address_family == socket.AF_INET:
  39. return ctypes.string_at(addr.ipv4_addr, 4)
  40. if address_family == socket.AF_INET6:
  41. return ctypes.string_at(addr.ipv6_addr, 16)
  42. raise socket.error('unknown address family')
  43. try:
  44. from urllib import unquote as urlunquote
  45. except ImportError:
  46. from urllib.parse import unquote_to_bytes as urlunquote
  47. from unicodedata import normalize
  48. unicode = type(u'')
  49. try:
  50. unichr
  51. except NameError:
  52. unichr = chr # py3
  53. NoneType = type(None)
  54. # from boltons.typeutils
  55. def make_sentinel(name='_MISSING', var_name=None):
  56. """Creates and returns a new **instance** of a new class, suitable for
  57. usage as a "sentinel", a kind of singleton often used to indicate
  58. a value is missing when ``None`` is a valid input.
  59. Args:
  60. name (str): Name of the Sentinel
  61. var_name (str): Set this name to the name of the variable in
  62. its respective module enable pickleability.
  63. >>> make_sentinel(var_name='_MISSING')
  64. _MISSING
  65. The most common use cases here in boltons are as default values
  66. for optional function arguments, partly because of its
  67. less-confusing appearance in automatically generated
  68. documentation. Sentinels also function well as placeholders in queues
  69. and linked lists.
  70. .. note::
  71. By design, additional calls to ``make_sentinel`` with the same
  72. values will not produce equivalent objects.
  73. >>> make_sentinel('TEST') == make_sentinel('TEST')
  74. False
  75. >>> type(make_sentinel('TEST')) == type(make_sentinel('TEST'))
  76. False
  77. """
  78. class Sentinel(object):
  79. def __init__(self):
  80. self.name = name
  81. self.var_name = var_name
  82. def __repr__(self):
  83. if self.var_name:
  84. return self.var_name
  85. return '%s(%r)' % (self.__class__.__name__, self.name)
  86. if var_name:
  87. def __reduce__(self):
  88. return self.var_name
  89. def __nonzero__(self):
  90. return False
  91. __bool__ = __nonzero__
  92. return Sentinel()
  93. _unspecified = _UNSET = make_sentinel('_UNSET')
  94. # RFC 3986 Section 2.3, Unreserved URI Characters
  95. # https://tools.ietf.org/html/rfc3986#section-2.3
  96. _UNRESERVED_CHARS = frozenset('~-._0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  97. 'abcdefghijklmnopqrstuvwxyz')
  98. # URL parsing regex (based on RFC 3986 Appendix B, with modifications)
  99. _URL_RE = re.compile(r'^((?P<scheme>[^:/?#]+):)?'
  100. r'((?P<_netloc_sep>//)(?P<authority>[^/?#]*))?'
  101. r'(?P<path>[^?#]*)'
  102. r'(\?(?P<query>[^#]*))?'
  103. r'(#(?P<fragment>.*))?')
  104. _SCHEME_RE = re.compile(r'^[a-zA-Z0-9+-.]*$')
  105. _HEX_CHAR_MAP = dict([((a + b).encode('ascii'),
  106. unichr(int(a + b, 16)).encode('charmap'))
  107. for a in string.hexdigits for b in string.hexdigits])
  108. _ASCII_RE = re.compile('([\x00-\x7f]+)')
  109. # RFC 3986 section 2.2, Reserved Characters
  110. # https://tools.ietf.org/html/rfc3986#section-2.2
  111. _GEN_DELIMS = frozenset(u':/?#[]@')
  112. _SUB_DELIMS = frozenset(u"!$&'()*+,;=")
  113. _ALL_DELIMS = _GEN_DELIMS | _SUB_DELIMS
  114. _USERINFO_SAFE = _UNRESERVED_CHARS | _SUB_DELIMS
  115. _USERINFO_DELIMS = _ALL_DELIMS - _USERINFO_SAFE
  116. _PATH_SAFE = _UNRESERVED_CHARS | _SUB_DELIMS | set(u':@%')
  117. _PATH_DELIMS = _ALL_DELIMS - _PATH_SAFE
  118. _SCHEMELESS_PATH_SAFE = _PATH_SAFE - set(':')
  119. _SCHEMELESS_PATH_DELIMS = _ALL_DELIMS - _SCHEMELESS_PATH_SAFE
  120. _FRAGMENT_SAFE = _UNRESERVED_CHARS | _PATH_SAFE | set(u'/?')
  121. _FRAGMENT_DELIMS = _ALL_DELIMS - _FRAGMENT_SAFE
  122. _QUERY_SAFE = _UNRESERVED_CHARS | _FRAGMENT_SAFE - set(u'&=+')
  123. _QUERY_DELIMS = _ALL_DELIMS - _QUERY_SAFE
  124. def _make_quote_map(safe_chars):
  125. ret = {}
  126. # v is included in the dict for py3 mostly, because bytestrings
  127. # are iterables of ints, of course!
  128. for i, v in zip(range(256), range(256)):
  129. c = chr(v)
  130. if c in safe_chars:
  131. ret[c] = ret[v] = c
  132. else:
  133. ret[c] = ret[v] = '%{0:02X}'.format(i)
  134. return ret
  135. _USERINFO_PART_QUOTE_MAP = _make_quote_map(_USERINFO_SAFE)
  136. _PATH_PART_QUOTE_MAP = _make_quote_map(_PATH_SAFE)
  137. _SCHEMELESS_PATH_PART_QUOTE_MAP = _make_quote_map(_SCHEMELESS_PATH_SAFE)
  138. _QUERY_PART_QUOTE_MAP = _make_quote_map(_QUERY_SAFE)
  139. _FRAGMENT_QUOTE_MAP = _make_quote_map(_FRAGMENT_SAFE)
  140. _ROOT_PATHS = frozenset(((), (u'',)))
  141. def _encode_path_part(text, maximal=True):
  142. "Percent-encode a single segment of a URL path."
  143. if maximal:
  144. bytestr = normalize('NFC', to_unicode(text)).encode('utf8')
  145. return u''.join([_PATH_PART_QUOTE_MAP[b] for b in bytestr])
  146. return u''.join([_PATH_PART_QUOTE_MAP[t] if t in _PATH_DELIMS else t
  147. for t in text])
  148. def _encode_schemeless_path_part(text, maximal=True):
  149. """Percent-encode the first segment of a URL path for a URL without a
  150. scheme specified.
  151. """
  152. if maximal:
  153. bytestr = normalize('NFC', to_unicode(text)).encode('utf8')
  154. return u''.join([_SCHEMELESS_PATH_PART_QUOTE_MAP[b] for b in bytestr])
  155. return u''.join([_SCHEMELESS_PATH_PART_QUOTE_MAP[t]
  156. if t in _SCHEMELESS_PATH_DELIMS else t for t in text])
  157. def _encode_path_parts(text_parts, rooted=False, has_scheme=True,
  158. has_authority=True, joined=True, maximal=True):
  159. """
  160. Percent-encode a tuple of path parts into a complete path.
  161. Setting *maximal* to False percent-encodes only the reserved
  162. characters that are syntactically necessary for serialization,
  163. preserving any IRI-style textual data.
  164. Leaving *maximal* set to its default True percent-encodes
  165. everything required to convert a portion of an IRI to a portion of
  166. a URI.
  167. RFC 3986 3.3:
  168. If a URI contains an authority component, then the path component
  169. must either be empty or begin with a slash ("/") character. If a URI
  170. does not contain an authority component, then the path cannot begin
  171. with two slash characters ("//"). In addition, a URI reference
  172. (Section 4.1) may be a relative-path reference, in which case the
  173. first path segment cannot contain a colon (":") character.
  174. """
  175. if not text_parts:
  176. return u'' if joined else text_parts
  177. if rooted:
  178. text_parts = (u'',) + text_parts
  179. # elif has_authority and text_parts:
  180. # raise Exception('see rfc above') # TODO: too late to fail like this?
  181. encoded_parts = []
  182. if has_scheme:
  183. encoded_parts = [_encode_path_part(part, maximal=maximal)
  184. if part else part for part in text_parts]
  185. else:
  186. encoded_parts = [_encode_schemeless_path_part(text_parts[0])]
  187. encoded_parts.extend([_encode_path_part(part, maximal=maximal)
  188. if part else part for part in text_parts[1:]])
  189. if joined:
  190. return u'/'.join(encoded_parts)
  191. return tuple(encoded_parts)
  192. def _encode_query_part(text, maximal=True):
  193. """
  194. Percent-encode a single query string key or value.
  195. """
  196. if maximal:
  197. bytestr = normalize('NFC', to_unicode(text)).encode('utf8')
  198. return u''.join([_QUERY_PART_QUOTE_MAP[b] for b in bytestr])
  199. return u''.join([_QUERY_PART_QUOTE_MAP[t] if t in _QUERY_DELIMS else t
  200. for t in text])
  201. def _encode_fragment_part(text, maximal=True):
  202. """Quote the fragment part of the URL. Fragments don't have
  203. subdelimiters, so the whole URL fragment can be passed.
  204. """
  205. if maximal:
  206. bytestr = normalize('NFC', to_unicode(text)).encode('utf8')
  207. return u''.join([_FRAGMENT_QUOTE_MAP[b] for b in bytestr])
  208. return u''.join([_FRAGMENT_QUOTE_MAP[t] if t in _FRAGMENT_DELIMS else t
  209. for t in text])
  210. def _encode_userinfo_part(text, maximal=True):
  211. """Quote special characters in either the username or password
  212. section of the URL.
  213. """
  214. if maximal:
  215. bytestr = normalize('NFC', to_unicode(text)).encode('utf8')
  216. return u''.join([_USERINFO_PART_QUOTE_MAP[b] for b in bytestr])
  217. return u''.join([_USERINFO_PART_QUOTE_MAP[t] if t in _USERINFO_DELIMS
  218. else t for t in text])
  219. # This port list painstakingly curated by hand searching through
  220. # https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
  221. # and
  222. # https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
  223. SCHEME_PORT_MAP = {'acap': 674, 'afp': 548, 'dict': 2628, 'dns': 53,
  224. 'file': None, 'ftp': 21, 'git': 9418, 'gopher': 70,
  225. 'http': 80, 'https': 443, 'imap': 143, 'ipp': 631,
  226. 'ipps': 631, 'irc': 194, 'ircs': 6697, 'ldap': 389,
  227. 'ldaps': 636, 'mms': 1755, 'msrp': 2855, 'msrps': None,
  228. 'mtqp': 1038, 'nfs': 111, 'nntp': 119, 'nntps': 563,
  229. 'pop': 110, 'prospero': 1525, 'redis': 6379, 'rsync': 873,
  230. 'rtsp': 554, 'rtsps': 322, 'rtspu': 5005, 'sftp': 22,
  231. 'smb': 445, 'snmp': 161, 'ssh': 22, 'steam': None,
  232. 'svn': 3690, 'telnet': 23, 'ventrilo': 3784, 'vnc': 5900,
  233. 'wais': 210, 'ws': 80, 'wss': 443, 'xmpp': None}
  234. # This list of schemes that don't use authorities is also from the link above.
  235. NO_NETLOC_SCHEMES = set(['urn', 'about', 'bitcoin', 'blob', 'data', 'geo',
  236. 'magnet', 'mailto', 'news', 'pkcs11',
  237. 'sip', 'sips', 'tel'])
  238. # As of Mar 11, 2017, there were 44 netloc schemes, and 13 non-netloc
  239. def register_scheme(text, uses_netloc=None, default_port=None):
  240. """Registers new scheme information, resulting in correct port and
  241. slash behavior from the URL object. There are dozens of standard
  242. schemes preregistered, so this function is mostly meant for
  243. proprietary internal customizations or stopgaps on missing
  244. standards information. If a scheme seems to be missing, please
  245. `file an issue`_!
  246. Args:
  247. text (str): Text representing the scheme.
  248. (the 'http' in 'http://hatnote.com')
  249. uses_netloc (bool): Does the scheme support specifying a
  250. network host? For instance, "http" does, "mailto" does not.
  251. default_port (int): The default port, if any, for netloc-using
  252. schemes.
  253. .. _file an issue: https://github.com/mahmoud/hyperlink/issues
  254. """
  255. text = text.lower()
  256. if default_port is not None:
  257. try:
  258. default_port = int(default_port)
  259. except ValueError:
  260. raise ValueError('default_port expected integer or None, not %r'
  261. % (default_port,))
  262. if uses_netloc is True:
  263. SCHEME_PORT_MAP[text] = default_port
  264. elif uses_netloc is False:
  265. if default_port is not None:
  266. raise ValueError('unexpected default port while specifying'
  267. ' non-netloc scheme: %r' % default_port)
  268. NO_NETLOC_SCHEMES.add(text)
  269. elif uses_netloc is not None:
  270. raise ValueError('uses_netloc expected True, False, or None')
  271. return
  272. def scheme_uses_netloc(scheme, default=None):
  273. """Whether or not a URL uses :code:`:` or :code:`://` to separate the
  274. scheme from the rest of the URL depends on the scheme's own
  275. standard definition. There is no way to infer this behavior
  276. from other parts of the URL. A scheme either supports network
  277. locations or it does not.
  278. The URL type's approach to this is to check for explicitly
  279. registered schemes, with common schemes like HTTP
  280. preregistered. This is the same approach taken by
  281. :mod:`urlparse`.
  282. URL adds two additional heuristics if the scheme as a whole is
  283. not registered. First, it attempts to check the subpart of the
  284. scheme after the last ``+`` character. This adds intuitive
  285. behavior for schemes like ``git+ssh``. Second, if a URL with
  286. an unrecognized scheme is loaded, it will maintain the
  287. separator it sees.
  288. """
  289. if not scheme:
  290. return False
  291. scheme = scheme.lower()
  292. if scheme in SCHEME_PORT_MAP:
  293. return True
  294. if scheme in NO_NETLOC_SCHEMES:
  295. return False
  296. if scheme.split('+')[-1] in SCHEME_PORT_MAP:
  297. return True
  298. return default
  299. class URLParseError(ValueError):
  300. """Exception inheriting from :exc:`ValueError`, raised when failing to
  301. parse a URL. Mostly raised on invalid ports and IPv6 addresses.
  302. """
  303. pass
  304. def _optional(argument, default):
  305. if argument is _UNSET:
  306. return default
  307. else:
  308. return argument
  309. def _typecheck(name, value, *types):
  310. """
  311. Check that the given *value* is one of the given *types*, or raise an
  312. exception describing the problem using *name*.
  313. """
  314. if not types:
  315. raise ValueError('expected one or more types, maybe use _textcheck?')
  316. if not isinstance(value, types):
  317. raise TypeError("expected %s for %s, got %r"
  318. % (" or ".join([t.__name__ for t in types]),
  319. name, value))
  320. return value
  321. def _textcheck(name, value, delims=frozenset(), nullable=False):
  322. if not isinstance(value, unicode):
  323. if nullable and value is None:
  324. return value # used by query string values
  325. else:
  326. str_name = "unicode" if bytes is str else "str"
  327. exp = str_name + ' or NoneType' if nullable else str_name
  328. raise TypeError('expected %s for %s, got %r' % (exp, name, value))
  329. if delims and set(value) & set(delims): # TODO: test caching into regexes
  330. raise ValueError('one or more reserved delimiters %s present in %s: %r'
  331. % (''.join(delims), name, value))
  332. return value
  333. def _percent_decode(text):
  334. """
  335. Replace percent-encoded characters with their UTF-8 equivalents.
  336. Args:
  337. text (unicode): The text with percent-encoded UTF-8 in it.
  338. Returns:
  339. unicode: The encoded version of *text*.
  340. """
  341. try:
  342. quotedBytes = text.encode("ascii")
  343. except UnicodeEncodeError:
  344. return text
  345. unquotedBytes = urlunquote(quotedBytes)
  346. try:
  347. return unquotedBytes.decode("utf-8")
  348. except UnicodeDecodeError:
  349. return text
  350. def _resolve_dot_segments(path):
  351. """Normalize the URL path by resolving segments of '.' and '..'. For
  352. more details, see `RFC 3986 section 5.2.4, Remove Dot Segments`_.
  353. Args:
  354. path (list): path segments in string form
  355. Returns:
  356. list: a new list of path segments with the '.' and '..' elements
  357. removed and resolved.
  358. .. _RFC 3986 section 5.2.4, Remove Dot Segments: https://tools.ietf.org/html/rfc3986#section-5.2.4
  359. """
  360. segs = []
  361. for seg in path:
  362. if seg == u'.':
  363. pass
  364. elif seg == u'..':
  365. if segs:
  366. segs.pop()
  367. else:
  368. segs.append(seg)
  369. if list(path[-1:]) in ([u'.'], [u'..']):
  370. segs.append(u'')
  371. return segs
  372. DEFAULT_ENCODING = 'utf8'
  373. def to_unicode(obj):
  374. try:
  375. return unicode(obj)
  376. except UnicodeDecodeError:
  377. return unicode(obj, encoding=DEFAULT_ENCODING)
  378. def parse_host(host):
  379. """Parse the host into a tuple of ``(family, host)``, where family
  380. is the appropriate :mod:`socket` module constant when the host is
  381. an IP address. Family is ``None`` when the host is not an IP.
  382. Will raise :class:`URLParseError` on invalid IPv6 constants.
  383. Returns:
  384. tuple: family (socket constant or None), host (string)
  385. >>> parse_host('googlewebsite.com') == (None, 'googlewebsite.com')
  386. True
  387. >>> parse_host('[::1]') == (socket.AF_INET6, '::1')
  388. True
  389. >>> parse_host('192.168.1.1') == (socket.AF_INET, '192.168.1.1')
  390. True
  391. """
  392. if not host:
  393. return None, u''
  394. if u':' in host and u'[' == host[0] and u']' == host[-1]:
  395. host = host[1:-1]
  396. try:
  397. inet_pton(socket.AF_INET6, host)
  398. except socket.error as se:
  399. raise URLParseError('invalid IPv6 host: %r (%r)' % (host, se))
  400. except UnicodeEncodeError:
  401. pass # TODO: this can't be a real host right?
  402. else:
  403. family = socket.AF_INET6
  404. return family, host
  405. try:
  406. inet_pton(socket.AF_INET, host)
  407. except (socket.error, UnicodeEncodeError):
  408. family = None # not an IP
  409. else:
  410. family = socket.AF_INET
  411. return family, host
  412. class URL(object):
  413. """From blogs to billboards, URLs are so common, that it's easy to
  414. overlook their complexity and power. With hyperlink's
  415. :class:`URL` type, working with URLs doesn't have to be hard.
  416. URLs are made of many parts. Most of these parts are officially
  417. named in `RFC 3986`_ and this diagram may prove handy in identifying
  418. them::
  419. foo://user:pass@example.com:8042/over/there?name=ferret#nose
  420. \_/ \_______/ \_________/ \__/\_________/ \_________/ \__/
  421. | | | | | | |
  422. scheme userinfo host port path query fragment
  423. While :meth:`~URL.from_text` is used for parsing whole URLs, the
  424. :class:`URL` constructor builds a URL from the individual
  425. components, like so::
  426. >>> from hyperlink import URL
  427. >>> url = URL(scheme=u'https', host=u'example.com', path=[u'hello', u'world'])
  428. >>> print(url.to_text())
  429. https://example.com/hello/world
  430. The constructor runs basic type checks. All strings are expected
  431. to be decoded (:class:`unicode` in Python 2). All arguments are
  432. optional, defaulting to appropriately empty values. A full list of
  433. constructor arguments is below.
  434. Args:
  435. scheme (str): The text name of the scheme.
  436. host (str): The host portion of the network location
  437. port (int): The port part of the network location. If
  438. ``None`` or no port is passed, the port will default to
  439. the default port of the scheme, if it is known. See the
  440. ``SCHEME_PORT_MAP`` and :func:`register_default_port`
  441. for more info.
  442. path (tuple): A tuple of strings representing the
  443. slash-separated parts of the path.
  444. query (tuple): The query parameters, as a tuple of
  445. key-value pairs.
  446. fragment (str): The fragment part of the URL.
  447. rooted (bool): Whether or not the path begins with a slash.
  448. userinfo (str): The username or colon-separated
  449. username:password pair.
  450. family: A socket module constant used when the host is an
  451. IP constant to differentiate IPv4 and domain names, as
  452. well as validate IPv6.
  453. uses_netloc (bool): Indicates whether two slashes appear
  454. between the scheme and the host (``http://eg.com`` vs
  455. ``mailto:e@g.com``). Set automatically based on scheme.
  456. All of these parts are also exposed as read-only attributes of
  457. URL instances, along with several useful methods.
  458. .. _RFC 3986: https://tools.ietf.org/html/rfc3986
  459. .. _RFC 3987: https://tools.ietf.org/html/rfc3987
  460. """
  461. def __init__(self, scheme=None, host=None, path=(), query=(), fragment=u'',
  462. port=None, rooted=None, userinfo=u'', family=None, uses_netloc=None):
  463. if host is not None and scheme is None:
  464. scheme = u'http' # TODO: why
  465. if port is None:
  466. port = SCHEME_PORT_MAP.get(scheme)
  467. if host and query and not path:
  468. # per RFC 3986 6.2.3, "a URI that uses the generic syntax
  469. # for authority with an empty path should be normalized to
  470. # a path of '/'."
  471. path = (u'',)
  472. # Now that we're done detecting whether they were passed, we can set
  473. # them to their defaults:
  474. if scheme is None:
  475. scheme = u''
  476. if host is None:
  477. host = u''
  478. if rooted is None:
  479. rooted = bool(host)
  480. # Set attributes.
  481. self._scheme = _textcheck("scheme", scheme)
  482. if self._scheme:
  483. if not _SCHEME_RE.match(self._scheme):
  484. raise ValueError('invalid scheme: %r. Only alphanumeric, "+",'
  485. ' "-", and "." allowed. Did you meant to call'
  486. ' %s.from_text()?'
  487. % (self._scheme, self.__class__.__name__))
  488. self._host = _textcheck("host", host, '/?#@')
  489. if isinstance(path, unicode):
  490. raise TypeError("expected iterable of text for path, not: %r"
  491. % (path,))
  492. self._path = tuple((_textcheck("path segment", segment, '/?#')
  493. for segment in path))
  494. self._query = tuple(
  495. (_textcheck("query parameter name", k, '&=#'),
  496. _textcheck("query parameter value", v, '&#', nullable=True))
  497. for (k, v) in query
  498. )
  499. self._fragment = _textcheck("fragment", fragment)
  500. self._port = _typecheck("port", port, int, NoneType)
  501. self._rooted = _typecheck("rooted", rooted, bool)
  502. self._userinfo = _textcheck("userinfo", userinfo, '/?#@')
  503. self._family = _typecheck("family", family,
  504. type(socket.AF_INET), NoneType)
  505. if ':' in self._host and self._family != socket.AF_INET6:
  506. raise ValueError('invalid ":" present in host: %r' % self._host)
  507. uses_netloc = scheme_uses_netloc(self._scheme, uses_netloc)
  508. self._uses_netloc = _typecheck("uses_netloc",
  509. uses_netloc, bool, NoneType)
  510. return
  511. @property
  512. def scheme(self):
  513. """The scheme is a string, and the first part of an absolute URL, the
  514. part before the first colon, and the part which defines the
  515. semantics of the rest of the URL. Examples include "http",
  516. "https", "ssh", "file", "mailto", and many others. See
  517. :func:`~hyperlink.register_scheme()` for more info.
  518. """
  519. return self._scheme
  520. @property
  521. def host(self):
  522. """The host is a string, and the second standard part of an absolute
  523. URL. When present, a valid host must be a domain name, or an
  524. IP (v4 or v6). It occurs before the first slash, or the second
  525. colon, if a :attr:`~hyperlink.URL.port` is provided.
  526. """
  527. return self._host
  528. @property
  529. def port(self):
  530. """The port is an integer that is commonly used in connecting to the
  531. :attr:`host`, and almost never appears without it.
  532. When not present in the original URL, this attribute defaults
  533. to the scheme's default port. If the scheme's default port is
  534. not known, and the port is not provided, this attribute will
  535. be set to None.
  536. >>> URL.from_text('http://example.com/pa/th').port
  537. 80
  538. >>> URL.from_text('foo://example.com/pa/th').port
  539. >>> URL.from_text('foo://example.com:8042/pa/th').port
  540. 8042
  541. .. note::
  542. Per the standard, when the port is the same as the schemes
  543. default port, it will be omitted in the text URL.
  544. """
  545. return self._port
  546. @property
  547. def path(self):
  548. """A tuple of strings, created by splitting the slash-separated
  549. hierarchical path. Started by the first slash after the host,
  550. terminated by a "?", which indicates the start of the
  551. :attr:`~hyperlink.URL.query` string.
  552. """
  553. return self._path
  554. @property
  555. def query(self):
  556. """Tuple of pairs, created by splitting the ampersand-separated
  557. mapping of keys and optional values representing
  558. non-hierarchical data used to identify the resource. Keys are
  559. always strings. Values are strings when present, or None when
  560. missing.
  561. For more operations on the mapping, see
  562. :meth:`~hyperlink.URL.get()`, :meth:`~hyperlink.URL.add()`,
  563. :meth:`~hyperlink.URL.set()`, and
  564. :meth:`~hyperlink.URL.delete()`.
  565. """
  566. return self._query
  567. @property
  568. def fragment(self):
  569. """A string, the last part of the URL, indicated by the first "#"
  570. after the :attr:`~hyperlink.URL.path` or
  571. :attr:`~hyperlink.URL.query`. Enables indirect identification
  572. of a secondary resource, like an anchor within an HTML page.
  573. """
  574. return self._fragment
  575. @property
  576. def rooted(self):
  577. """Whether or not the path starts with a forward slash (``/``).
  578. This is taken from the terminology in the BNF grammar,
  579. specifically the "path-rootless", rule, since "absolute path"
  580. and "absolute URI" are somewhat ambiguous. :attr:`path` does
  581. not contain the implicit prefixed ``"/"`` since that is
  582. somewhat awkward to work with.
  583. """
  584. return self._rooted
  585. @property
  586. def userinfo(self):
  587. """The colon-separated string forming the username-password
  588. combination.
  589. """
  590. return self._userinfo
  591. @property
  592. def family(self):
  593. """Set to a socket constant (:data:`socket.AF_INET` or
  594. :data:`socket.AF_INET6`) when the :attr:`~hyperlink.URL.host`
  595. is an IP address. Set to ``None`` if the host is a domain name or
  596. not set.
  597. """
  598. return self._family
  599. @property
  600. def uses_netloc(self):
  601. """
  602. """
  603. return self._uses_netloc
  604. @property
  605. def user(self):
  606. """
  607. The user portion of :attr:`~hyperlink.URL.userinfo`.
  608. """
  609. return self.userinfo.split(u':')[0]
  610. def authority(self, with_password=False, **kw):
  611. """Compute and return the appropriate host/port/userinfo combination.
  612. >>> url = URL.from_text(u'http://user:pass@localhost:8080/a/b?x=y')
  613. >>> url.authority()
  614. u'user:@localhost:8080'
  615. >>> url.authority(with_password=True)
  616. u'user:pass@localhost:8080'
  617. Args:
  618. with_password (bool): Whether the return value of this
  619. method include the password in the URL, if it is
  620. set. Defaults to False.
  621. Returns:
  622. str: The authority (network location and user information) portion
  623. of the URL.
  624. """
  625. # first, a bit of twisted compat
  626. with_password = kw.pop('includeSecrets', with_password)
  627. if kw:
  628. raise TypeError('got unexpected keyword arguments: %r' % kw.keys())
  629. if self.family == socket.AF_INET6:
  630. hostport = ['[' + self.host + ']']
  631. else:
  632. hostport = [self.host]
  633. if self.port != SCHEME_PORT_MAP.get(self.scheme):
  634. hostport.append(unicode(self.port))
  635. authority = []
  636. if self.userinfo:
  637. userinfo = self.userinfo
  638. if not with_password and u":" in userinfo:
  639. userinfo = userinfo[:userinfo.index(u":") + 1]
  640. authority.append(userinfo)
  641. authority.append(u":".join(hostport))
  642. return u"@".join(authority)
  643. def __eq__(self, other):
  644. if not isinstance(other, self.__class__):
  645. return NotImplemented
  646. for attr in ['scheme', 'userinfo', 'host', 'query',
  647. 'fragment', 'port', 'family', 'uses_netloc']:
  648. if getattr(self, attr) != getattr(other, attr):
  649. return False
  650. if self.path == other.path or (self.path in _ROOT_PATHS
  651. and other.path in _ROOT_PATHS):
  652. return True
  653. return False
  654. def __ne__(self, other):
  655. if not isinstance(other, self.__class__):
  656. return NotImplemented
  657. return not self.__eq__(other)
  658. def __hash__(self):
  659. return hash((self.__class__, self.scheme, self.userinfo, self.host,
  660. self.path, self.query, self.fragment, self.port,
  661. self.rooted, self.family, self.uses_netloc))
  662. @property
  663. def absolute(self):
  664. """Whether or not the URL is "absolute". Absolute URLs are complete
  665. enough to resolve to a network resource without being relative
  666. to a base URI.
  667. >>> URL.from_text('http://wikipedia.org/').absolute
  668. True
  669. >>> URL.from_text('?a=b&c=d').absolute
  670. False
  671. Absolute URLs must have both a scheme and a host set.
  672. """
  673. return bool(self.scheme and self.host)
  674. def replace(self, scheme=_UNSET, host=_UNSET, path=_UNSET, query=_UNSET,
  675. fragment=_UNSET, port=_UNSET, rooted=_UNSET, userinfo=_UNSET):
  676. """:class:`URL` objects are immutable, which means that attributes
  677. are designed to be set only once, at construction. Instead of
  678. modifying an existing URL, one simply creates a copy with the
  679. desired changes.
  680. If any of the following arguments is omitted, it defaults to
  681. the value on the current URL.
  682. Args:
  683. scheme (str): The text name of the scheme.
  684. host (str): The host portion of the network location
  685. port (int): The port part of the network location.
  686. path (tuple): A tuple of strings representing the
  687. slash-separated parts of the path.
  688. query (tuple): The query parameters, as a tuple of
  689. key-value pairs.
  690. fragment (str): The fragment part of the URL.
  691. rooted (bool): Whether or not the path begins with a slash.
  692. userinfo (str): The username or colon-separated
  693. username:password pair.
  694. family: A socket module constant used when the host is an
  695. IP constant to differentiate IPv4 and domain names, as
  696. well as validate IPv6.
  697. uses_netloc (bool): Indicates whether two slashes appear
  698. between the scheme and the host (``http://eg.com`` vs
  699. ``mailto:e@g.com``)
  700. Returns:
  701. URL: a copy of the current :class:`URL`, with new values for
  702. parameters passed.
  703. """
  704. return self.__class__(
  705. scheme=_optional(scheme, self.scheme),
  706. host=_optional(host, self.host),
  707. path=_optional(path, self.path),
  708. query=_optional(query, self.query),
  709. fragment=_optional(fragment, self.fragment),
  710. port=_optional(port, self.port),
  711. rooted=_optional(rooted, self.rooted),
  712. userinfo=_optional(userinfo, self.userinfo),
  713. )
  714. @classmethod
  715. def from_text(cls, text):
  716. """Whereas the :class:`URL` constructor is useful for constructing
  717. URLs from parts, :meth:`~URL.from_text` supports parsing whole
  718. URLs from their string form::
  719. >>> URL.from_text('http://example.com')
  720. URL.from_text('http://example.com')
  721. >>> URL.from_text('?a=b&x=y')
  722. URL.from_text('?a=b&x=y')
  723. As you can see above, it's also used as the :func:`repr` of
  724. :class:`URL` objects. The natural counterpart to
  725. :func:`~URL.to_text()`.
  726. Args:
  727. text (str): A valid URL string.
  728. Returns:
  729. URL: The structured object version of the parsed string.
  730. Somewhat unexpectedly, URLs are a far more permissive format
  731. than most would assume. Many strings which don't look like
  732. URLs are still valid URLs. As a result, this method only
  733. raises :class:`URLParseError` on invalid port and IPv6 values
  734. in the host portion of the URL.
  735. """
  736. s = to_unicode(text)
  737. um = _URL_RE.match(s)
  738. try:
  739. gs = um.groupdict()
  740. except AttributeError:
  741. raise URLParseError('could not parse url: %r' % s)
  742. au_text = gs['authority']
  743. userinfo, hostinfo = u'', au_text
  744. if au_text:
  745. userinfo, sep, hostinfo = au_text.rpartition('@')
  746. host, port = None, None
  747. if hostinfo:
  748. host, sep, port_str = hostinfo.rpartition(u':')
  749. if not sep:
  750. host = port_str
  751. else:
  752. if u']' in port_str:
  753. host = hostinfo # wrong split, was an ipv6
  754. else:
  755. try:
  756. port = int(port_str)
  757. except ValueError:
  758. if not port_str: # TODO: excessive?
  759. raise URLParseError('port must not be empty')
  760. raise URLParseError('expected integer for port, not %r'
  761. % port_str)
  762. family, host = parse_host(host)
  763. scheme = gs['scheme'] or u''
  764. fragment = gs['fragment'] or u''
  765. uses_netloc = bool(gs['_netloc_sep'])
  766. if gs['path']:
  767. path = gs['path'].split(u"/")
  768. if not path[0]:
  769. path.pop(0)
  770. rooted = True
  771. else:
  772. rooted = False
  773. else:
  774. path = ()
  775. rooted = bool(hostinfo)
  776. if gs['query']:
  777. query = ((qe.split(u"=", 1) if u'=' in qe else (qe, None))
  778. for qe in gs['query'].split(u"&"))
  779. else:
  780. query = ()
  781. return cls(scheme, host, path, query, fragment, port,
  782. rooted, userinfo, family, uses_netloc)
  783. def child(self, *segments):
  784. """Make a new :class:`URL` where the given path segments are a child
  785. of this URL, preserving other parts of the URL, including the
  786. query string and fragment.
  787. For example::
  788. >>> url = URL.from_text(u"http://localhost/a/b?x=y")
  789. >>> child_url = url.child(u"c", u"d")
  790. >>> child_url.to_text()
  791. u'http://localhost/a/b/c/d?x=y'
  792. Args:
  793. segments (str): Additional parts to be joined and added to
  794. the path, like :func:`os.path.join`. Special characters
  795. in segments will be percent encoded.
  796. Returns:
  797. URL: A copy of the current URL with the extra path segments.
  798. """
  799. segments = [_textcheck('path segment', s) for s in segments]
  800. new_segs = _encode_path_parts(segments, joined=False, maximal=False)
  801. new_path = self.path[:-1 if (self.path and self.path[-1] == u'')
  802. else None] + new_segs
  803. return self.replace(path=new_path)
  804. def sibling(self, segment):
  805. """Make a new :class:`URL` with a single path segment that is a
  806. sibling of this URL path.
  807. Args:
  808. segment (str): A single path segment.
  809. Returns:
  810. URL: A copy of the current URL with the last path segment
  811. replaced by *segment*. Special characters such as
  812. ``/?#`` will be percent encoded.
  813. """
  814. _textcheck('path segment', segment)
  815. new_path = self.path[:-1] + (_encode_path_part(segment),)
  816. return self.replace(path=new_path)
  817. def click(self, href=u''):
  818. """Resolve the given URL relative to this URL.
  819. The resulting URI should match what a web browser would
  820. generate if you visited the current URL and clicked on *href*.
  821. >>> url = URL.from_text('http://blog.hatnote.com/')
  822. >>> url.click(u'/post/155074058790').to_text()
  823. u'http://blog.hatnote.com/post/155074058790'
  824. >>> url = URL.from_text('http://localhost/a/b/c/')
  825. >>> url.click(u'../d/./e').to_text()
  826. u'http://localhost/a/b/d/e'
  827. Args:
  828. href (str): A string representing a clicked URL.
  829. Return:
  830. URL: A copy of the current URL with navigation logic applied.
  831. For more information, see `RFC 3986 section 5`_.
  832. .. _RFC 3986 section 5: https://tools.ietf.org/html/rfc3986#section-5
  833. """
  834. _textcheck("relative URL", href)
  835. if href:
  836. clicked = URL.from_text(href)
  837. if clicked.absolute:
  838. return clicked
  839. else:
  840. clicked = self
  841. query = clicked.query
  842. if clicked.scheme and not clicked.rooted:
  843. # Schemes with relative paths are not well-defined. RFC 3986 calls
  844. # them a "loophole in prior specifications" that should be avoided,
  845. # or supported only for backwards compatibility.
  846. raise NotImplementedError('absolute URI with rootless path: %r'
  847. % (href,))
  848. else:
  849. if clicked.rooted:
  850. path = clicked.path
  851. elif clicked.path:
  852. path = self.path[:-1] + clicked.path
  853. else:
  854. path = self.path
  855. if not query:
  856. query = self.query
  857. return self.replace(scheme=clicked.scheme or self.scheme,
  858. host=clicked.host or self.host,
  859. port=clicked.port or self.port,
  860. path=_resolve_dot_segments(path),
  861. query=query,
  862. fragment=clicked.fragment)
  863. def to_uri(self):
  864. u"""Make a new :class:`URL` instance with all non-ASCII characters
  865. appropriately percent-encoded. This is useful to do in preparation
  866. for sending a :class:`URL` over a network protocol.
  867. For example::
  868. >>> URL.from_text(u"https://→example.com/foo⇧bar/").to_uri()
  869. URL.from_text(u'https://xn--example-dk9c.com/foo%E2%87%A7bar/')
  870. Returns:
  871. URL: A new instance with its path segments, query parameters, and
  872. hostname encoded, so that they are all in the standard
  873. US-ASCII range.
  874. """
  875. new_userinfo = u':'.join([_encode_userinfo_part(p) for p in
  876. self.userinfo.split(':', 1)])
  877. new_path = _encode_path_parts(self.path, has_scheme=bool(self.scheme),
  878. rooted=False, joined=False, maximal=True)
  879. return self.replace(
  880. userinfo=new_userinfo,
  881. host=self.host.encode("idna").decode("ascii"),
  882. path=new_path,
  883. query=tuple([tuple(_encode_query_part(x, maximal=True)
  884. if x is not None else None
  885. for x in (k, v))
  886. for k, v in self.query]),
  887. fragment=_encode_fragment_part(self.fragment, maximal=True)
  888. )
  889. def to_iri(self):
  890. u"""Make a new :class:`URL` instance with all but a few reserved
  891. characters decoded into human-readable format.
  892. Percent-encoded Unicode and IDNA-encoded hostnames are
  893. decoded, like so::
  894. >>> url = URL.from_text(u'https://xn--example-dk9c.com/foo%E2%87%A7bar/')
  895. >>> print(url.to_iri().to_text())
  896. https://→example.com/foo⇧bar/
  897. .. note::
  898. As a general Python issue, "narrow" (UCS-2) builds of
  899. Python may not be able to fully decode certain URLs, and
  900. the in those cases, this method will return a best-effort,
  901. partially-decoded, URL which is still valid. This issue
  902. does not affect any Python builds 3.4+.
  903. Returns:
  904. URL: A new instance with its path segments, query parameters, and
  905. hostname decoded for display purposes.
  906. """
  907. new_userinfo = u':'.join([_percent_decode(p) for p in
  908. self.userinfo.split(':', 1)])
  909. try:
  910. asciiHost = self.host.encode("ascii")
  911. except UnicodeEncodeError:
  912. textHost = self.host
  913. else:
  914. try:
  915. textHost = asciiHost.decode("idna")
  916. except ValueError:
  917. # only reached on "narrow" (UCS-2) Python builds <3.4, see #7
  918. textHost = self.host
  919. return self.replace(userinfo=new_userinfo,
  920. host=textHost,
  921. path=[_percent_decode(segment)
  922. for segment in self.path],
  923. query=[tuple(_percent_decode(x)
  924. if x is not None else None
  925. for x in (k, v))
  926. for k, v in self.query],
  927. fragment=_percent_decode(self.fragment))
  928. def to_text(self, with_password=False):
  929. """Render this URL to its textual representation.
  930. By default, the URL text will *not* include a password, if one
  931. is set. RFC 3986 considers using URLs to represent such
  932. sensitive information as deprecated. Quoting from RFC 3986,
  933. `section 3.2.1`:
  934. "Applications should not render as clear text any data after the
  935. first colon (":") character found within a userinfo subcomponent
  936. unless the data after the colon is the empty string (indicating no
  937. password)."
  938. Args:
  939. with_password (bool): Whether or not to include the
  940. password in the URL text. Defaults to False.
  941. Returns:
  942. str: The serialized textual representation of this URL,
  943. such as ``u"http://example.com/some/path?some=query"``.
  944. The natural counterpart to :class:`URL.from_text()`.
  945. .. _section 3.2.1: https://tools.ietf.org/html/rfc3986#section-3.2.1
  946. """
  947. scheme = self.scheme
  948. authority = self.authority(with_password)
  949. path = _encode_path_parts(self.path,
  950. rooted=self.rooted,
  951. has_scheme=bool(scheme),
  952. has_authority=bool(authority),
  953. maximal=False)
  954. query_string = u'&'.join(
  955. u'='.join((_encode_query_part(x, maximal=False)
  956. for x in ([k] if v is None else [k, v])))
  957. for (k, v) in self.query)
  958. fragment = self.fragment
  959. parts = []
  960. _add = parts.append
  961. if scheme:
  962. _add(scheme)
  963. _add(':')
  964. if authority:
  965. _add('//')
  966. _add(authority)
  967. elif (scheme and path[:2] != '//' and self.uses_netloc):
  968. _add('//')
  969. if path:
  970. if scheme and authority and path[:1] != '/':
  971. _add('/') # relpaths with abs authorities auto get '/'
  972. _add(path)
  973. if query_string:
  974. _add('?')
  975. _add(query_string)
  976. if fragment:
  977. _add('#')
  978. _add(fragment)
  979. return u''.join(parts)
  980. def __repr__(self):
  981. """Convert this URL to an representation that shows all of its
  982. constituent parts, as well as being a valid argument to
  983. :func:`eval`.
  984. """
  985. return '%s.from_text(%r)' % (self.__class__.__name__, self.to_text())
  986. # # Begin Twisted Compat Code
  987. asURI = to_uri
  988. asIRI = to_iri
  989. @classmethod
  990. def fromText(cls, s):
  991. return cls.from_text(s)
  992. def asText(self, includeSecrets=False):
  993. return self.to_text(with_password=includeSecrets)
  994. def __dir__(self):
  995. try:
  996. ret = object.__dir__(self)
  997. except AttributeError:
  998. # object.__dir__ == AttributeError # pdw for py2
  999. ret = dir(self.__class__) + list(self.__dict__.keys())
  1000. ret = sorted(set(ret) - set(['fromText', 'asURI', 'asIRI', 'asText']))
  1001. return ret
  1002. # # End Twisted Compat Code
  1003. def add(self, name, value=None):
  1004. """Make a new :class:`URL` instance with a given query argument,
  1005. *name*, added to it with the value *value*, like so::
  1006. >>> URL.from_text(u'https://example.com/?x=y').add(u'x')
  1007. URL.from_text(u'https://example.com/?x=y&x')
  1008. >>> URL.from_text(u'https://example.com/?x=y').add(u'x', u'z')
  1009. URL.from_text(u'https://example.com/?x=y&x=z')
  1010. Args:
  1011. name (str): The name of the query parameter to add. The
  1012. part before the ``=``.
  1013. value (str): The value of the query parameter to add. The
  1014. part after the ``=``. Defaults to ``None``, meaning no
  1015. value.
  1016. Returns:
  1017. URL: A new :class:`URL` instance with the parameter added.
  1018. """
  1019. return self.replace(query=self.query + ((name, value),))
  1020. def set(self, name, value=None):
  1021. """Make a new :class:`URL` instance with the query parameter *name*
  1022. set to *value*. All existing occurences, if any are replaced
  1023. by the single name-value pair.
  1024. >>> URL.from_text(u'https://example.com/?x=y').set(u'x')
  1025. URL.from_text(u'https://example.com/?x')
  1026. >>> URL.from_text(u'https://example.com/?x=y').set(u'x', u'z')
  1027. URL.from_text(u'https://example.com/?x=z')
  1028. Args:
  1029. name (str): The name of the query parameter to set. The
  1030. part before the ``=``.
  1031. value (str): The value of the query parameter to set. The
  1032. part after the ``=``. Defaults to ``None``, meaning no
  1033. value.
  1034. Returns:
  1035. URL: A new :class:`URL` instance with the parameter set.
  1036. """
  1037. # Preserve the original position of the query key in the list
  1038. q = [(k, v) for (k, v) in self.query if k != name]
  1039. idx = next((i for (i, (k, v)) in enumerate(self.query)
  1040. if k == name), -1)
  1041. q[idx:idx] = [(name, value)]
  1042. return self.replace(query=q)
  1043. def get(self, name):
  1044. """Get a list of values for the given query parameter, *name*::
  1045. >>> url = URL.from_text('?x=1&x=2')
  1046. >>> url.get('x')
  1047. [u'1', u'2']
  1048. >>> url.get('y')
  1049. []
  1050. If the given *name* is not set, an empty list is returned. A
  1051. list is always returned, and this method raises no exceptions.
  1052. Args:
  1053. name (str): The name of the query parameter to get.
  1054. Returns:
  1055. list: A list of all the values associated with the key, in
  1056. string form.
  1057. """
  1058. return [value for (key, value) in self.query if name == key]
  1059. def remove(self, name):
  1060. """Make a new :class:`URL` instance with all occurrences of the query
  1061. parameter *name* removed. No exception is raised if the
  1062. parameter is not already set.
  1063. Args:
  1064. name (str): The name of the query parameter to remove.
  1065. Returns:
  1066. URL: A new :class:`URL` instance with the parameter removed.
  1067. """
  1068. return self.replace(query=((k, v) for (k, v) in self.query
  1069. if k != name))