blob: 044c8109a50c5dd23d179238da86996a50f04482 [file] [log] [blame]
Jeremy Hylton1afc1692008-06-18 20:49:58 +00001"""Parse (absolute and relative) URLs.
2
3See RFC 1808: "Relative Uniform Resource Locators", by R. Fielding,
4UC Irvine, June 1995.
5"""
6
Facundo Batista2ac5de22008-07-07 18:24:11 +00007import sys
Guido van Rossum52dbbb92008-08-18 21:44:30 +00008import collections
Facundo Batista2ac5de22008-07-07 18:24:11 +00009
Jeremy Hylton1afc1692008-06-18 20:49:58 +000010__all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag",
Facundo Batistac469d4c2008-09-03 22:49:01 +000011 "urlsplit", "urlunsplit", "parse_qs", "parse_qsl",
Guido van Rossum52dbbb92008-08-18 21:44:30 +000012 "quote", "quote_plus", "quote_from_bytes",
13 "unquote", "unquote_plus", "unquote_to_bytes"]
Jeremy Hylton1afc1692008-06-18 20:49:58 +000014
15# A classification of schemes ('' means apply by default)
16uses_relative = ['ftp', 'http', 'gopher', 'nntp', 'imap',
17 'wais', 'file', 'https', 'shttp', 'mms',
18 'prospero', 'rtsp', 'rtspu', '', 'sftp']
19uses_netloc = ['ftp', 'http', 'gopher', 'nntp', 'telnet',
20 'imap', 'wais', 'file', 'mms', 'https', 'shttp',
21 'snews', 'prospero', 'rtsp', 'rtspu', 'rsync', '',
22 'svn', 'svn+ssh', 'sftp']
23non_hierarchical = ['gopher', 'hdl', 'mailto', 'news',
24 'telnet', 'wais', 'imap', 'snews', 'sip', 'sips']
25uses_params = ['ftp', 'hdl', 'prospero', 'http', 'imap',
26 'https', 'shttp', 'rtsp', 'rtspu', 'sip', 'sips',
27 'mms', '', 'sftp']
28uses_query = ['http', 'wais', 'imap', 'https', 'shttp', 'mms',
29 'gopher', 'rtsp', 'rtspu', 'sip', 'sips', '']
30uses_fragment = ['ftp', 'hdl', 'http', 'gopher', 'news',
31 'nntp', 'wais', 'https', 'shttp', 'snews',
32 'file', 'prospero', '']
33
34# Characters valid in scheme names
35scheme_chars = ('abcdefghijklmnopqrstuvwxyz'
36 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
37 '0123456789'
38 '+-.')
39
40MAX_CACHE_SIZE = 20
41_parse_cache = {}
42
43def clear_cache():
44 """Clear the parse cache."""
45 _parse_cache.clear()
46
47
48class ResultMixin(object):
49 """Shared methods for the parsed result objects."""
50
51 @property
52 def username(self):
53 netloc = self.netloc
54 if "@" in netloc:
55 userinfo = netloc.rsplit("@", 1)[0]
56 if ":" in userinfo:
57 userinfo = userinfo.split(":", 1)[0]
58 return userinfo
59 return None
60
61 @property
62 def password(self):
63 netloc = self.netloc
64 if "@" in netloc:
65 userinfo = netloc.rsplit("@", 1)[0]
66 if ":" in userinfo:
67 return userinfo.split(":", 1)[1]
68 return None
69
70 @property
71 def hostname(self):
72 netloc = self.netloc
73 if "@" in netloc:
74 netloc = netloc.rsplit("@", 1)[1]
75 if ":" in netloc:
76 netloc = netloc.split(":", 1)[0]
77 return netloc.lower() or None
78
79 @property
80 def port(self):
81 netloc = self.netloc
82 if "@" in netloc:
83 netloc = netloc.rsplit("@", 1)[1]
84 if ":" in netloc:
85 port = netloc.split(":", 1)[1]
86 return int(port, 10)
87 return None
88
89from collections import namedtuple
90
91class SplitResult(namedtuple('SplitResult', 'scheme netloc path query fragment'), ResultMixin):
92
93 __slots__ = ()
94
95 def geturl(self):
96 return urlunsplit(self)
97
98
99class ParseResult(namedtuple('ParseResult', 'scheme netloc path params query fragment'), ResultMixin):
100
101 __slots__ = ()
102
103 def geturl(self):
104 return urlunparse(self)
105
106
107def urlparse(url, scheme='', allow_fragments=True):
108 """Parse a URL into 6 components:
109 <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
110 Return a 6-tuple: (scheme, netloc, path, params, query, fragment).
111 Note that we don't break the components up in smaller bits
112 (e.g. netloc is a single string) and we don't expand % escapes."""
113 tuple = urlsplit(url, scheme, allow_fragments)
114 scheme, netloc, url, query, fragment = tuple
115 if scheme in uses_params and ';' in url:
116 url, params = _splitparams(url)
117 else:
118 params = ''
119 return ParseResult(scheme, netloc, url, params, query, fragment)
120
121def _splitparams(url):
122 if '/' in url:
123 i = url.find(';', url.rfind('/'))
124 if i < 0:
125 return url, ''
126 else:
127 i = url.find(';')
128 return url[:i], url[i+1:]
129
130def _splitnetloc(url, start=0):
131 delim = len(url) # position of end of domain part of url, default is end
132 for c in '/?#': # look for delimiters; the order is NOT important
133 wdelim = url.find(c, start) # find first of this delim
134 if wdelim >= 0: # if found
135 delim = min(delim, wdelim) # use earliest delim position
136 return url[start:delim], url[delim:] # return (domain, rest)
137
138def urlsplit(url, scheme='', allow_fragments=True):
139 """Parse a URL into 5 components:
140 <scheme>://<netloc>/<path>?<query>#<fragment>
141 Return a 5-tuple: (scheme, netloc, path, query, fragment).
142 Note that we don't break the components up in smaller bits
143 (e.g. netloc is a single string) and we don't expand % escapes."""
144 allow_fragments = bool(allow_fragments)
145 key = url, scheme, allow_fragments, type(url), type(scheme)
146 cached = _parse_cache.get(key, None)
147 if cached:
148 return cached
149 if len(_parse_cache) >= MAX_CACHE_SIZE: # avoid runaway growth
150 clear_cache()
151 netloc = query = fragment = ''
152 i = url.find(':')
153 if i > 0:
154 if url[:i] == 'http': # optimize the common case
155 scheme = url[:i].lower()
156 url = url[i+1:]
157 if url[:2] == '//':
158 netloc, url = _splitnetloc(url, 2)
159 if allow_fragments and '#' in url:
160 url, fragment = url.split('#', 1)
161 if '?' in url:
162 url, query = url.split('?', 1)
163 v = SplitResult(scheme, netloc, url, query, fragment)
164 _parse_cache[key] = v
165 return v
166 for c in url[:i]:
167 if c not in scheme_chars:
168 break
169 else:
170 scheme, url = url[:i].lower(), url[i+1:]
171 if scheme in uses_netloc and url[:2] == '//':
172 netloc, url = _splitnetloc(url, 2)
173 if allow_fragments and scheme in uses_fragment and '#' in url:
174 url, fragment = url.split('#', 1)
175 if scheme in uses_query and '?' in url:
176 url, query = url.split('?', 1)
177 v = SplitResult(scheme, netloc, url, query, fragment)
178 _parse_cache[key] = v
179 return v
180
181def urlunparse(components):
182 """Put a parsed URL back together again. This may result in a
183 slightly different, but equivalent URL, if the URL that was parsed
184 originally had redundant delimiters, e.g. a ? with an empty query
185 (the draft states that these are equivalent)."""
186 scheme, netloc, url, params, query, fragment = components
187 if params:
188 url = "%s;%s" % (url, params)
189 return urlunsplit((scheme, netloc, url, query, fragment))
190
191def urlunsplit(components):
192 scheme, netloc, url, query, fragment = components
193 if netloc or (scheme and scheme in uses_netloc and url[:2] != '//'):
194 if url and url[:1] != '/': url = '/' + url
195 url = '//' + (netloc or '') + url
196 if scheme:
197 url = scheme + ':' + url
198 if query:
199 url = url + '?' + query
200 if fragment:
201 url = url + '#' + fragment
202 return url
203
204def urljoin(base, url, allow_fragments=True):
205 """Join a base URL and a possibly relative URL to form an absolute
206 interpretation of the latter."""
207 if not base:
208 return url
209 if not url:
210 return base
211 bscheme, bnetloc, bpath, bparams, bquery, bfragment = \
212 urlparse(base, '', allow_fragments)
213 scheme, netloc, path, params, query, fragment = \
214 urlparse(url, bscheme, allow_fragments)
215 if scheme != bscheme or scheme not in uses_relative:
216 return url
217 if scheme in uses_netloc:
218 if netloc:
219 return urlunparse((scheme, netloc, path,
220 params, query, fragment))
221 netloc = bnetloc
222 if path[:1] == '/':
223 return urlunparse((scheme, netloc, path,
224 params, query, fragment))
Facundo Batista23e38562008-08-14 16:55:14 +0000225 if not path:
226 path = bpath
227 if not params:
228 params = bparams
229 else:
230 path = path[:-1]
231 return urlunparse((scheme, netloc, path,
232 params, query, fragment))
233 if not query:
234 query = bquery
235 return urlunparse((scheme, netloc, path,
236 params, query, fragment))
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000237 segments = bpath.split('/')[:-1] + path.split('/')
238 # XXX The stuff below is bogus in various ways...
239 if segments[-1] == '.':
240 segments[-1] = ''
241 while '.' in segments:
242 segments.remove('.')
243 while 1:
244 i = 1
245 n = len(segments) - 1
246 while i < n:
247 if (segments[i] == '..'
248 and segments[i-1] not in ('', '..')):
249 del segments[i-1:i+1]
250 break
251 i = i+1
252 else:
253 break
254 if segments == ['', '..']:
255 segments[-1] = ''
256 elif len(segments) >= 2 and segments[-1] == '..':
257 segments[-2:] = ['']
258 return urlunparse((scheme, netloc, '/'.join(segments),
259 params, query, fragment))
260
261def urldefrag(url):
262 """Removes any existing fragment from URL.
263
264 Returns a tuple of the defragmented URL and the fragment. If
265 the URL contained no fragments, the second element is the
266 empty string.
267 """
268 if '#' in url:
269 s, n, p, a, q, frag = urlparse(url)
270 defrag = urlunparse((s, n, p, a, q, ''))
271 return defrag, frag
272 else:
273 return url, ''
274
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000275def unquote_to_bytes(string):
276 """unquote_to_bytes('abc%20def') -> b'abc def'."""
277 # Note: strings are encoded as UTF-8. This is only an issue if it contains
278 # unescaped non-ASCII characters, which URIs should not.
279 if isinstance(string, str):
280 string = string.encode('utf-8')
281 res = string.split(b'%')
282 res[0] = res[0]
Guido van Rossumdf9f1ec2008-08-06 19:31:34 +0000283 for i in range(1, len(res)):
284 item = res[i]
285 try:
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000286 res[i] = bytes([int(item[:2], 16)]) + item[2:]
287 except ValueError:
288 res[i] = b'%' + item
289 return b''.join(res)
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000290
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000291def unquote(string, encoding='utf-8', errors='replace'):
292 """Replace %xx escapes by their single-character equivalent. The optional
293 encoding and errors parameters specify how to decode percent-encoded
294 sequences into Unicode characters, as accepted by the bytes.decode()
295 method.
296 By default, percent-encoded sequences are decoded with UTF-8, and invalid
297 sequences are replaced by a placeholder character.
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000298
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000299 unquote('abc%20def') -> 'abc def'.
300 """
301 if encoding is None: encoding = 'utf-8'
302 if errors is None: errors = 'replace'
303 # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
304 # (list of single-byte bytes objects)
305 pct_sequence = []
306 res = string.split('%')
307 for i in range(1, len(res)):
308 item = res[i]
309 try:
310 if not item: raise ValueError
311 pct_sequence.append(bytes.fromhex(item[:2]))
312 rest = item[2:]
313 except ValueError:
314 rest = '%' + item
315 if not rest:
316 # This segment was just a single percent-encoded character.
317 # May be part of a sequence of code units, so delay decoding.
318 # (Stored in pct_sequence).
319 res[i] = ''
320 else:
321 # Encountered non-percent-encoded characters. Flush the current
322 # pct_sequence.
323 res[i] = b''.join(pct_sequence).decode(encoding, errors) + rest
324 pct_sequence = []
325 if pct_sequence:
326 # Flush the final pct_sequence
327 # res[-1] will always be empty if pct_sequence != []
328 assert not res[-1], "string=%r, res=%r" % (string, res)
329 res[-1] = b''.join(pct_sequence).decode(encoding, errors)
330 return ''.join(res)
331
Facundo Batistac469d4c2008-09-03 22:49:01 +0000332def parse_qs(qs, keep_blank_values=0, strict_parsing=0):
333 """Parse a query given as a string argument.
334
335 Arguments:
336
337 qs: URL-encoded query string to be parsed
338
339 keep_blank_values: flag indicating whether blank values in
340 URL encoded queries should be treated as blank strings.
341 A true value indicates that blanks should be retained as
342 blank strings. The default false value indicates that
343 blank values are to be ignored and treated as if they were
344 not included.
345
346 strict_parsing: flag indicating what to do with parsing errors.
347 If false (the default), errors are silently ignored.
348 If true, errors raise a ValueError exception.
349 """
350 dict = {}
351 for name, value in parse_qsl(qs, keep_blank_values, strict_parsing):
352 if name in dict:
353 dict[name].append(value)
354 else:
355 dict[name] = [value]
356 return dict
357
358def parse_qsl(qs, keep_blank_values=0, strict_parsing=0):
359 """Parse a query given as a string argument.
360
361 Arguments:
362
363 qs: URL-encoded query string to be parsed
364
365 keep_blank_values: flag indicating whether blank values in
366 URL encoded queries should be treated as blank strings. A
367 true value indicates that blanks should be retained as blank
368 strings. The default false value indicates that blank values
369 are to be ignored and treated as if they were not included.
370
371 strict_parsing: flag indicating what to do with parsing errors. If
372 false (the default), errors are silently ignored. If true,
373 errors raise a ValueError exception.
374
375 Returns a list, as G-d intended.
376 """
377 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
378 r = []
379 for name_value in pairs:
380 if not name_value and not strict_parsing:
381 continue
382 nv = name_value.split('=', 1)
383 if len(nv) != 2:
384 if strict_parsing:
385 raise ValueError("bad query field: %r" % (name_value,))
386 # Handle case of a control-name with no equal sign
387 if keep_blank_values:
388 nv.append('')
389 else:
390 continue
391 if len(nv[1]) or keep_blank_values:
392 name = unquote(nv[0].replace('+', ' '))
393 value = unquote(nv[1].replace('+', ' '))
394 r.append((name, value))
395
396 return r
397
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000398def unquote_plus(string, encoding='utf-8', errors='replace'):
399 """Like unquote(), but also replace plus signs by spaces, as required for
400 unquoting HTML form values.
401
402 unquote_plus('%7e/abc+def') -> '~/abc def'
403 """
404 string = string.replace('+', ' ')
405 return unquote(string, encoding, errors)
406
407_ALWAYS_SAFE = frozenset(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
408 b'abcdefghijklmnopqrstuvwxyz'
409 b'0123456789'
410 b'_.-')
Guido van Rossumdf9f1ec2008-08-06 19:31:34 +0000411_safe_quoters= {}
412
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000413class Quoter(collections.defaultdict):
414 """A mapping from bytes (in range(0,256)) to strings.
415
416 String values are percent-encoded byte values, unless the key < 128, and
417 in the "safe" set (either the specified safe set, or default set).
418 """
419 # Keeps a cache internally, using defaultdict, for efficiency (lookups
420 # of cached keys don't call Python code at all).
Guido van Rossumdf9f1ec2008-08-06 19:31:34 +0000421 def __init__(self, safe):
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000422 """safe: bytes object."""
423 self.safe = _ALWAYS_SAFE.union(c for c in safe if c < 128)
Guido van Rossumdf9f1ec2008-08-06 19:31:34 +0000424
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000425 def __repr__(self):
426 # Without this, will just display as a defaultdict
427 return "<Quoter %r>" % dict(self)
Guido van Rossumdf9f1ec2008-08-06 19:31:34 +0000428
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000429 def __missing__(self, b):
430 # Handle a cache miss. Store quoted string in cache and return.
431 res = b in self.safe and chr(b) or ('%%%02X' % b)
432 self[b] = res
433 return res
434
435def quote(string, safe='/', encoding=None, errors=None):
Guido van Rossumdf9f1ec2008-08-06 19:31:34 +0000436 """quote('abc def') -> 'abc%20def'
437
438 Each part of a URL, e.g. the path info, the query, etc., has a
439 different set of reserved characters that must be quoted.
440
441 RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
442 the following reserved characters.
443
444 reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
445 "$" | ","
446
447 Each of these characters is reserved in some component of a URL,
448 but not necessarily in all of them.
449
450 By default, the quote function is intended for quoting the path
451 section of a URL. Thus, it will not encode '/'. This character
452 is reserved, but in typical usage the quote function is being
453 called on a path where the existing slash characters are used as
454 reserved characters.
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000455
456 string and safe may be either str or bytes objects. encoding must
457 not be specified if string is a str.
458
459 The optional encoding and errors parameters specify how to deal with
460 non-ASCII characters, as accepted by the str.encode method.
461 By default, encoding='utf-8' (characters are encoded with UTF-8), and
462 errors='strict' (unsupported characters raise a UnicodeEncodeError).
Guido van Rossumdf9f1ec2008-08-06 19:31:34 +0000463 """
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000464 if isinstance(string, str):
465 if encoding is None:
466 encoding = 'utf-8'
467 if errors is None:
468 errors = 'strict'
469 string = string.encode(encoding, errors)
470 else:
471 if encoding is not None:
472 raise TypeError("quote() doesn't support 'encoding' for bytes")
473 if errors is not None:
474 raise TypeError("quote() doesn't support 'errors' for bytes")
475 return quote_from_bytes(string, safe)
476
477def quote_plus(string, safe='', encoding=None, errors=None):
478 """Like quote(), but also replace ' ' with '+', as required for quoting
479 HTML form values. Plus signs in the original string are escaped unless
480 they are included in safe. It also does not have safe default to '/'.
481 """
Jeremy Hyltonf8198862009-03-26 16:55:08 +0000482 # Check if ' ' in string, where string may either be a str or bytes. If
483 # there are no spaces, the regular quote will produce the right answer.
484 if ((isinstance(string, str) and ' ' not in string) or
485 (isinstance(string, bytes) and b' ' not in string)):
486 return quote(string, safe, encoding, errors)
487 if isinstance(safe, str):
488 space = ' '
489 else:
490 space = b' '
491 string = quote(string, safe + space)
492 return string.replace(' ', '+')
Guido van Rossum52dbbb92008-08-18 21:44:30 +0000493
494def quote_from_bytes(bs, safe='/'):
495 """Like quote(), but accepts a bytes object rather than a str, and does
496 not perform string-to-bytes encoding. It always returns an ASCII string.
497 quote_from_bytes(b'abc def\xab') -> 'abc%20def%AB'
498 """
499 if isinstance(safe, str):
500 # Normalize 'safe' by converting to bytes and removing non-ASCII chars
501 safe = safe.encode('ascii', 'ignore')
502 cachekey = bytes(safe) # In case it was a bytearray
503 if not (isinstance(bs, bytes) or isinstance(bs, bytearray)):
504 raise TypeError("quote_from_bytes() expected a bytes")
Guido van Rossumdf9f1ec2008-08-06 19:31:34 +0000505 try:
506 quoter = _safe_quoters[cachekey]
507 except KeyError:
508 quoter = Quoter(safe)
509 _safe_quoters[cachekey] = quoter
Jeremy Hyltonf8198862009-03-26 16:55:08 +0000510 return ''.join([quoter[char] for char in bs])
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000511
Jeremy Hyltona4de60a2009-03-26 14:49:26 +0000512def urlencode(query, doseq=0):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000513 """Encode a sequence of two-element tuples or dictionary into a URL query string.
514
515 If any values in the query arg are sequences and doseq is true, each
516 sequence element is converted to a separate parameter.
517
518 If the query arg is a sequence of two-element tuples, the order of the
519 parameters in the output will match the order of parameters in the
520 input.
521 """
522
Jeremy Hyltona4de60a2009-03-26 14:49:26 +0000523 if hasattr(query, "items"):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000524 # mapping objects
525 query = query.items()
526 else:
527 # it's a bother at times that strings and string-like objects are
528 # sequences...
529 try:
530 # non-sequence items should not work with len()
531 # non-empty strings will fail this
532 if len(query) and not isinstance(query[0], tuple):
533 raise TypeError
534 # zero-length sequences of all types will get here and succeed,
535 # but that's a minor nit - since the original implementation
536 # allowed empty dicts that type of behavior probably should be
537 # preserved for consistency
538 except TypeError:
Jeremy Hyltona4de60a2009-03-26 14:49:26 +0000539 ty, va, tb = sys.exc_info()
540 raise TypeError("not a valid non-string sequence "
541 "or mapping object").with_traceback(tb)
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000542
543 l = []
544 if not doseq:
545 # preserve old behavior
546 for k, v in query:
547 k = quote_plus(str(k))
548 v = quote_plus(str(v))
549 l.append(k + '=' + v)
550 else:
551 for k, v in query:
552 k = quote_plus(str(k))
553 if isinstance(v, str):
554 v = quote_plus(v)
555 l.append(k + '=' + v)
556 elif isinstance(v, str):
557 # is there a reasonable way to convert to ASCII?
558 # encode generates a string, but "replace" or "ignore"
559 # lose information and "strict" can raise UnicodeError
Jeremy Hyltona4de60a2009-03-26 14:49:26 +0000560 v = quote_plus(v.encode("ASCII", "replace"))
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000561 l.append(k + '=' + v)
562 else:
563 try:
564 # is this a sufficient test for sequence-ness?
565 x = len(v)
566 except TypeError:
567 # not a sequence
568 v = quote_plus(str(v))
569 l.append(k + '=' + v)
570 else:
571 # loop over the sequence
572 for elt in v:
573 l.append(k + '=' + quote_plus(str(elt)))
574 return '&'.join(l)
575
576# Utilities to parse URLs (most of these return None for missing parts):
577# unwrap('<URL:type://host/path>') --> 'type://host/path'
578# splittype('type:opaquestring') --> 'type', 'opaquestring'
579# splithost('//host[:port]/path') --> 'host[:port]', '/path'
580# splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'
581# splitpasswd('user:passwd') -> 'user', 'passwd'
582# splitport('host:port') --> 'host', 'port'
583# splitquery('/path?query') --> '/path', 'query'
584# splittag('/path#tag') --> '/path', 'tag'
585# splitattr('/path;attr1=value1;attr2=value2;...') ->
586# '/path', ['attr1=value1', 'attr2=value2', ...]
587# splitvalue('attr=value') --> 'attr', 'value'
588# urllib.parse.unquote('abc%20def') -> 'abc def'
589# quote('abc def') -> 'abc%20def')
590
Georg Brandl13e89462008-07-01 19:56:00 +0000591def to_bytes(url):
592 """to_bytes(u"URL") --> 'URL'."""
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000593 # Most URL schemes require ASCII. If that changes, the conversion
594 # can be relaxed.
Georg Brandl13e89462008-07-01 19:56:00 +0000595 # XXX get rid of to_bytes()
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000596 if isinstance(url, str):
597 try:
598 url = url.encode("ASCII").decode()
599 except UnicodeError:
600 raise UnicodeError("URL " + repr(url) +
601 " contains non-ASCII characters")
602 return url
603
604def unwrap(url):
605 """unwrap('<URL:type://host/path>') --> 'type://host/path'."""
606 url = str(url).strip()
607 if url[:1] == '<' and url[-1:] == '>':
608 url = url[1:-1].strip()
609 if url[:4] == 'URL:': url = url[4:].strip()
610 return url
611
612_typeprog = None
613def splittype(url):
614 """splittype('type:opaquestring') --> 'type', 'opaquestring'."""
615 global _typeprog
616 if _typeprog is None:
617 import re
618 _typeprog = re.compile('^([^/:]+):')
619
620 match = _typeprog.match(url)
621 if match:
622 scheme = match.group(1)
623 return scheme.lower(), url[len(scheme) + 1:]
624 return None, url
625
626_hostprog = None
627def splithost(url):
628 """splithost('//host[:port]/path') --> 'host[:port]', '/path'."""
629 global _hostprog
630 if _hostprog is None:
631 import re
632 _hostprog = re.compile('^//([^/?]*)(.*)$')
633
634 match = _hostprog.match(url)
635 if match: return match.group(1, 2)
636 return None, url
637
638_userprog = None
639def splituser(host):
640 """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'."""
641 global _userprog
642 if _userprog is None:
643 import re
644 _userprog = re.compile('^(.*)@(.*)$')
645
646 match = _userprog.match(host)
Guido van Rossumdf9f1ec2008-08-06 19:31:34 +0000647 if match: return map(unquote, match.group(1, 2))
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000648 return None, host
649
650_passwdprog = None
651def splitpasswd(user):
652 """splitpasswd('user:passwd') -> 'user', 'passwd'."""
653 global _passwdprog
654 if _passwdprog is None:
655 import re
656 _passwdprog = re.compile('^([^:]*):(.*)$')
657
658 match = _passwdprog.match(user)
659 if match: return match.group(1, 2)
660 return user, None
661
662# splittag('/path#tag') --> '/path', 'tag'
663_portprog = None
664def splitport(host):
665 """splitport('host:port') --> 'host', 'port'."""
666 global _portprog
667 if _portprog is None:
668 import re
669 _portprog = re.compile('^(.*):([0-9]+)$')
670
671 match = _portprog.match(host)
672 if match: return match.group(1, 2)
673 return host, None
674
675_nportprog = None
676def splitnport(host, defport=-1):
677 """Split host and port, returning numeric port.
678 Return given default port if no ':' found; defaults to -1.
679 Return numerical port if a valid number are found after ':'.
680 Return None if ':' but not a valid number."""
681 global _nportprog
682 if _nportprog is None:
683 import re
684 _nportprog = re.compile('^(.*):(.*)$')
685
686 match = _nportprog.match(host)
687 if match:
688 host, port = match.group(1, 2)
689 try:
690 if not port: raise ValueError("no digits")
691 nport = int(port)
692 except ValueError:
693 nport = None
694 return host, nport
695 return host, defport
696
697_queryprog = None
698def splitquery(url):
699 """splitquery('/path?query') --> '/path', 'query'."""
700 global _queryprog
701 if _queryprog is None:
702 import re
703 _queryprog = re.compile('^(.*)\?([^?]*)$')
704
705 match = _queryprog.match(url)
706 if match: return match.group(1, 2)
707 return url, None
708
709_tagprog = None
710def splittag(url):
711 """splittag('/path#tag') --> '/path', 'tag'."""
712 global _tagprog
713 if _tagprog is None:
714 import re
715 _tagprog = re.compile('^(.*)#([^#]*)$')
716
717 match = _tagprog.match(url)
718 if match: return match.group(1, 2)
719 return url, None
720
721def splitattr(url):
722 """splitattr('/path;attr1=value1;attr2=value2;...') ->
723 '/path', ['attr1=value1', 'attr2=value2', ...]."""
724 words = url.split(';')
725 return words[0], words[1:]
726
727_valueprog = None
728def splitvalue(attr):
729 """splitvalue('attr=value') --> 'attr', 'value'."""
730 global _valueprog
731 if _valueprog is None:
732 import re
733 _valueprog = re.compile('^([^=]*)=(.*)$')
734
735 match = _valueprog.match(attr)
736 if match: return match.group(1, 2)
737 return attr, None
738
739test_input = """
740 http://a/b/c/d
741
742 g:h = <URL:g:h>
743 http:g = <URL:http://a/b/c/g>
744 http: = <URL:http://a/b/c/d>
745 g = <URL:http://a/b/c/g>
746 ./g = <URL:http://a/b/c/g>
747 g/ = <URL:http://a/b/c/g/>
748 /g = <URL:http://a/g>
749 //g = <URL:http://g>
750 ?y = <URL:http://a/b/c/d?y>
751 g?y = <URL:http://a/b/c/g?y>
752 g?y/./x = <URL:http://a/b/c/g?y/./x>
753 . = <URL:http://a/b/c/>
754 ./ = <URL:http://a/b/c/>
755 .. = <URL:http://a/b/>
756 ../ = <URL:http://a/b/>
757 ../g = <URL:http://a/b/g>
758 ../.. = <URL:http://a/>
759 ../../g = <URL:http://a/g>
760 ../../../g = <URL:http://a/../g>
761 ./../g = <URL:http://a/b/g>
762 ./g/. = <URL:http://a/b/c/g/>
763 /./g = <URL:http://a/./g>
764 g/./h = <URL:http://a/b/c/g/h>
765 g/../h = <URL:http://a/b/c/h>
766 http:g = <URL:http://a/b/c/g>
767 http: = <URL:http://a/b/c/d>
768 http:?y = <URL:http://a/b/c/d?y>
769 http:g?y = <URL:http://a/b/c/g?y>
770 http:g?y/./x = <URL:http://a/b/c/g?y/./x>
771"""
772
773def test():
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000774 base = ''
775 if sys.argv[1:]:
776 fn = sys.argv[1]
777 if fn == '-':
778 fp = sys.stdin
779 else:
780 fp = open(fn)
781 else:
782 from io import StringIO
783 fp = StringIO(test_input)
784 for line in fp:
785 words = line.split()
786 if not words:
787 continue
788 url = words[0]
789 parts = urlparse(url)
790 print('%-10s : %s' % (url, parts))
791 abs = urljoin(base, url)
792 if not base:
793 base = abs
794 wrapped = '<URL:%s>' % abs
795 print('%-10s = %s' % (url, wrapped))
796 if len(words) == 3 and words[1] == '=':
797 if wrapped != words[2]:
798 print('EXPECTED', words[2], '!!!!!!!!!!')
799
800if __name__ == '__main__':
801 test()