/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/urlutils.py

  • Committer: Jelmer Vernooij
  • Date: 2020-03-22 01:35:14 UTC
  • mfrom: (7490.7.6 work)
  • mto: This revision was merged to the branch mainline in revision 7499.
  • Revision ID: jelmer@jelmer.uk-20200322013514-7vw1ntwho04rcuj3
merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
"""A collection of function for handling URL operations."""
18
18
 
19
 
from __future__ import absolute_import
20
 
 
21
19
import os
22
20
import re
23
21
import sys
24
22
 
25
 
try:
26
 
    import urlparse
27
 
except ImportError:
28
 
    from urllib import parse as urlparse
 
23
from urllib import parse as urlparse
29
24
 
30
25
from . import (
31
26
    errors,
37
32
from posixpath import split as _posix_split
38
33
""")
39
34
 
40
 
from .sixish import (
41
 
    PY3,
42
 
    text_type,
43
 
    )
44
35
 
45
36
 
46
37
class InvalidURL(errors.PathError):
66
57
    def __init__(self, from_, to):
67
58
        self.from_ = from_
68
59
        self.to = to
69
 
        errors.PathError.__init__(self, from_, 'URLs differ by more than path.')
 
60
        errors.PathError.__init__(
 
61
            self, from_, 'URLs differ by more than path.')
70
62
 
71
63
 
72
64
def basename(url, exclude_trailing_slash=True):
96
88
    return split(url, exclude_trailing_slash=exclude_trailing_slash)[0]
97
89
 
98
90
 
99
 
# Private copies of quote and unquote, copied from Python's
100
 
# urllib module because urllib unconditionally imports socket, which imports
101
 
# ssl.
102
 
 
103
 
always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
104
 
               'abcdefghijklmnopqrstuvwxyz'
105
 
               '0123456789' '_.-')
106
 
_safe_map = {}
107
 
for i, c in zip(range(256), ''.join(map(chr, range(256)))):
108
 
    _safe_map[c] = c if (i < 128 and c in always_safe) else '%{0:02X}'.format(i)
109
 
_safe_quoters = {}
110
 
 
111
 
 
112
 
def quote(s, safe='/'):
113
 
    """quote('abc def') -> 'abc%20def'
114
 
 
115
 
    Each part of a URL, e.g. the path info, the query, etc., has a
116
 
    different set of reserved characters that must be quoted.
117
 
 
118
 
    RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
119
 
    the following reserved characters.
120
 
 
121
 
    reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
122
 
                  "$" | ","
123
 
 
124
 
    Each of these characters is reserved in some component of a URL,
125
 
    but not necessarily in all of them.
126
 
 
127
 
    By default, the quote function is intended for quoting the path
128
 
    section of a URL.  Thus, it will not encode '/'.  This character
129
 
    is reserved, but in typical usage the quote function is being
130
 
    called on a path where the existing slash characters are used as
131
 
    reserved characters.
132
 
    """
133
 
    # fastpath
134
 
    if not s:
135
 
        if s is None:
136
 
            raise TypeError('None object cannot be quoted')
137
 
        return s
138
 
    cachekey = (safe, always_safe)
139
 
    try:
140
 
        (quoter, safe) = _safe_quoters[cachekey]
141
 
    except KeyError:
142
 
        safe_map = _safe_map.copy()
143
 
        safe_map.update([(c, c) for c in safe])
144
 
        quoter = safe_map.__getitem__
145
 
        safe = always_safe + safe
146
 
        _safe_quoters[cachekey] = (quoter, safe)
147
 
    if not s.rstrip(safe):
148
 
        return s
149
 
    return ''.join(map(quoter, s))
150
 
 
151
 
 
 
91
quote_from_bytes = urlparse.quote_from_bytes
 
92
quote = urlparse.quote
 
93
unquote_to_bytes = urlparse.unquote_to_bytes
152
94
unquote = urlparse.unquote
153
95
 
154
96
 
155
 
def escape(relpath):
 
97
def escape(relpath, safe='/~'):
156
98
    """Escape relpath to be a valid url."""
157
 
    if not isinstance(relpath, str):
158
 
        relpath = relpath.encode('utf-8')
159
 
    return quote(relpath, safe='/~')
 
99
    return quote(relpath, safe=safe)
160
100
 
161
101
 
162
102
def file_relpath(base, path):
166
106
    """
167
107
    if len(base) < MIN_ABS_FILEURL_LENGTH:
168
108
        raise ValueError('Length of base (%r) must equal or'
169
 
            ' exceed the platform minimum url length (which is %d)' %
170
 
            (base, MIN_ABS_FILEURL_LENGTH))
 
109
                         ' exceed the platform minimum url length (which is %d)' %
 
110
                         (base, MIN_ABS_FILEURL_LENGTH))
171
111
    base = osutils.normpath(local_path_from_url(base))
172
112
    path = osutils.normpath(local_path_from_url(path))
173
113
    return escape(osutils.relpath(base, path))
191
131
    first_path_slash = path.find('/')
192
132
    if first_path_slash == -1:
193
133
        return len(scheme), None
194
 
    return len(scheme), first_path_slash+m.start('path')
 
134
    return len(scheme), first_path_slash + m.start('path')
195
135
 
196
136
 
197
137
def is_url(url):
247
187
    """
248
188
    path = base.split('/')
249
189
    if len(path) > 1 and path[-1] == '':
250
 
        #If the path ends in a trailing /, remove it.
 
190
        # If the path ends in a trailing /, remove it.
251
191
        path.pop()
252
192
    for arg in args:
253
193
        if arg.startswith('/'):
258
198
            elif chunk == '..':
259
199
                if path == ['']:
260
200
                    raise InvalidURLJoin('Cannot go above root',
261
 
                            base, args)
 
201
                                         base, args)
262
202
                path.pop()
263
203
            else:
264
204
                path.append(chunk)
271
211
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
272
212
def _posix_local_path_from_url(url):
273
213
    """Convert a url like file:///path/to/foo into /path/to/foo"""
274
 
    url = split_segment_parameters_raw(url)[0]
 
214
    url = strip_segment_parameters(url)
275
215
    file_localhost_prefix = 'file://localhost/'
276
216
    if url.startswith(file_localhost_prefix):
277
217
        path = url[len(file_localhost_prefix) - 1:]
298
238
    """Convert a url like file:///C:/path/to/foo into C:/path/to/foo"""
299
239
    if not url.startswith('file://'):
300
240
        raise InvalidURL(url, 'local urls must start with file:///, '
301
 
                                     'UNC path urls must start with file://')
302
 
    url = split_segment_parameters_raw(url)[0]
 
241
                         'UNC path urls must start with file://')
 
242
    url = strip_segment_parameters(url)
303
243
    # We strip off all 3 slashes
304
244
    win32_url = url[len('file:'):]
305
245
    # check for UNC path: //HOST/path
306
246
    if not win32_url.startswith('///'):
307
247
        if (win32_url[2] == '/'
308
 
            or win32_url[3] in '|:'):
 
248
                or win32_url[3] in '|:'):
309
249
            raise InvalidURL(url, 'Win32 UNC path urls'
310
 
                ' have form file://HOST/path')
 
250
                             ' have form file://HOST/path')
311
251
        return unescape(win32_url)
312
252
 
313
253
    # allow empty paths so we can serve all roots
317
257
    # usual local path with drive letter
318
258
    if (len(win32_url) < 6
319
259
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
320
 
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
321
 
        or win32_url[4] not in  '|:'
322
 
        or win32_url[5] != '/'):
 
260
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ') or
 
261
        win32_url[4] not in '|:'
 
262
            or win32_url[5] != '/'):
323
263
        raise InvalidURL(url, 'Win32 file urls start with'
324
 
                ' file:///x:/, where x is a valid drive letter')
 
264
                         ' file:///x:/, where x is a valid drive letter')
325
265
    return win32_url[3].upper() + u':' + unescape(win32_url[5:])
326
266
 
327
267
 
344
284
    if win32_path.startswith('//'):
345
285
        return 'file:' + escape(win32_path)
346
286
    return ('file:///' + str(win32_path[0].upper()) + ':' +
347
 
        escape(win32_path[2:]))
 
287
            escape(win32_path[2:]))
348
288
 
349
289
 
350
290
local_path_to_url = _posix_local_path_to_url
400
340
        return local_path_to_url(url)
401
341
    prefix = url[:path_start]
402
342
    path = url[path_start:]
403
 
    if not isinstance(url, text_type):
 
343
    if not isinstance(url, str):
404
344
        for c in url:
405
345
            if c not in _url_safe_characters:
406
346
                raise InvalidURL(url, 'URLs can only contain specific'
407
 
                                            ' safe characters (not %r)' % c)
 
347
                                 ' safe characters (not %r)' % c)
408
348
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
409
349
        return str(prefix + ''.join(path))
410
350
 
413
353
 
414
354
    for i in range(len(path_chars)):
415
355
        if path_chars[i] not in _url_safe_characters:
416
 
            chars = path_chars[i].encode('utf-8')
417
356
            path_chars[i] = ''.join(
418
 
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
 
357
                ['%%%02X' % c for c in bytearray(path_chars[i].encode('utf-8'))])
419
358
    path = ''.join(path_chars)
420
359
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
421
360
    return str(prefix + path)
441
380
    if base_scheme != other_scheme:
442
381
        return other
443
382
    elif sys.platform == 'win32' and base_scheme == 'file://':
444
 
        base_drive = base[base_first_slash+1:base_first_slash+3]
445
 
        other_drive = other[other_first_slash+1:other_first_slash+3]
 
383
        base_drive = base[base_first_slash + 1:base_first_slash + 3]
 
384
        other_drive = other[other_first_slash + 1:other_first_slash + 3]
446
385
        if base_drive != other_drive:
447
386
            return other
448
387
 
449
 
    base_path = base[base_first_slash+1:]
450
 
    other_path = other[other_first_slash+1:]
 
388
    base_path = base[base_first_slash + 1:]
 
389
    other_path = other[other_first_slash + 1:]
451
390
 
452
391
    if base_path.endswith('/'):
453
392
        base_path = base_path[:-1]
479
418
    # path is currently /C:/foo
480
419
    if len(path) < 4 or path[2] not in ':|' or path[3] != '/':
481
420
        raise InvalidURL(url_base + path,
482
 
            'win32 file:/// paths need a drive letter')
483
 
    url_base += path[0:3] # file:// + /C:
484
 
    path = path[3:] # /foo
 
421
                         'win32 file:/// paths need a drive letter')
 
422
    url_base += path[0:3]  # file:// + /C:
 
423
    path = path[3:]  # /foo
485
424
    return url_base, path
486
425
 
487
426
 
492
431
    :param exclude_trailing_slash: Strip off a final '/' if it is part
493
432
        of the path (but not if it is part of the protocol specification)
494
433
 
495
 
    :return: (parent_url, child_dir).  child_dir may be the empty string if we're at
496
 
        the root.
 
434
    :return: (parent_url, child_dir).  child_dir may be the empty string if
 
435
        we're at the root.
497
436
    """
498
437
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
499
438
 
509
448
            return url, ''
510
449
 
511
450
    # We have a fully defined path
512
 
    url_base = url[:first_path_slash] # http://host, file://
513
 
    path = url[first_path_slash:] # /file/foo
 
451
    url_base = url[:first_path_slash]  # http://host, file://
 
452
    path = url[first_path_slash:]  # /file/foo
514
453
 
515
454
    if sys.platform == 'win32' and url.startswith('file:///'):
516
455
        # Strip off the drive letter
533
472
    """
534
473
    # GZ 2011-11-18: Dodgy removing the terminal slash like this, function
535
474
    #                operates on urls not url+segments, and Transport classes
536
 
    #                should not be blindly adding slashes in the first place. 
 
475
    #                should not be blindly adding slashes in the first place.
537
476
    lurl = strip_trailing_slash(url)
538
477
    # Segments begin at first comma after last forward slash, if one exists
539
 
    segment_start = lurl.find(",", lurl.rfind("/")+1)
 
478
    segment_start = lurl.find(",", lurl.rfind("/") + 1)
540
479
    if segment_start == -1:
541
480
        return (url, [])
542
 
    return (lurl[:segment_start], lurl[segment_start+1:].split(","))
 
481
    return (lurl[:segment_start],
 
482
            [str(s) for s in lurl[segment_start + 1:].split(",")])
543
483
 
544
484
 
545
485
def split_segment_parameters(url):
551
491
    (base_url, subsegments) = split_segment_parameters_raw(url)
552
492
    parameters = {}
553
493
    for subsegment in subsegments:
554
 
        (key, value) = subsegment.split("=", 1)
 
494
        try:
 
495
            (key, value) = subsegment.split("=", 1)
 
496
        except ValueError:
 
497
            raise InvalidURL(url, "missing = in subsegment")
 
498
        if not isinstance(key, str):
 
499
            raise TypeError(key)
 
500
        if not isinstance(value, str):
 
501
            raise TypeError(value)
555
502
        parameters[key] = value
556
503
    return (base_url, parameters)
557
504
 
558
505
 
 
506
def strip_segment_parameters(url):
 
507
    """Strip the segment parameters from a URL.
 
508
 
 
509
    :param url: A relative or absolute URL
 
510
    :return: url
 
511
    """
 
512
    base_url, subsegments = split_segment_parameters_raw(url)
 
513
    return base_url
 
514
 
 
515
 
559
516
def join_segment_parameters_raw(base, *subsegments):
560
 
    """Create a new URL by adding subsegments to an existing one. 
 
517
    """Create a new URL by adding subsegments to an existing one.
561
518
 
562
519
    This adds the specified subsegments to the last path in the specified
563
520
    base URL. The subsegments should be bytestrings.
571
528
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
572
529
        if "," in subsegment:
573
530
            raise InvalidURLJoin(", exists in subsegments",
574
 
                                        base, subsegments)
 
531
                                 base, subsegments)
575
532
    return ",".join((base,) + subsegments)
576
533
 
577
534
 
589
546
    new_parameters.update(existing_parameters)
590
547
    for key, value in parameters.items():
591
548
        if not isinstance(key, str):
592
 
            raise TypeError("parameter key %r is not a bytestring" % key)
 
549
            raise TypeError("parameter key %r is not a str" % key)
593
550
        if not isinstance(value, str):
594
 
            raise TypeError("parameter value %r for %s is not a bytestring" %
595
 
                (key, value))
 
551
            raise TypeError("parameter value %r for %r is not a str" %
 
552
                            (value, key))
596
553
        if "=" in key:
597
554
            raise InvalidURLJoin("= exists in parameter key", url,
598
 
                parameters)
 
555
                                 parameters)
599
556
        new_parameters[key] = value
600
 
    return join_segment_parameters_raw(base, 
601
 
        *["%s=%s" % item for item in sorted(new_parameters.items())])
 
557
    return join_segment_parameters_raw(
 
558
        base, *["%s=%s" % item for item in sorted(new_parameters.items())])
602
559
 
603
560
 
604
561
def _win32_strip_local_trailing_slash(url):
642
599
        # so just chop off the last character
643
600
        return url[:-1]
644
601
 
645
 
    if first_path_slash is None or first_path_slash == len(url)-1:
 
602
    if first_path_slash is None or first_path_slash == len(url) - 1:
646
603
        # Don't chop off anything if the only slash is the path
647
604
        # separating slash
648
605
        return url
662
619
    #       plain ASCII strings, or the final .decode will
663
620
    #       try to encode the UNICODE => ASCII, and then decode
664
621
    #       it into utf-8.
665
 
    if isinstance(url, text_type):
 
622
 
 
623
    if isinstance(url, str):
666
624
        try:
667
 
            url = url.encode("ascii")
 
625
            url.encode("ascii")
668
626
        except UnicodeError as e:
669
 
            raise InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
670
 
    if PY3:
671
 
        unquoted = urlparse.unquote_to_bytes(url)
672
 
    else:
673
 
        unquoted = unquote(url)
674
 
    try:
675
 
        unicode_path = unquoted.decode('utf-8')
676
 
    except UnicodeError as e:
677
 
        raise InvalidURL(url, 'Unable to encode the URL as utf-8: %s' % (e,))
678
 
    return unicode_path
 
627
            raise InvalidURL(
 
628
                url, 'URL was not a plain ASCII url: %s' % (e,))
 
629
    return urlparse.unquote(url)
679
630
 
680
631
 
681
632
# These are characters that if escaped, should stay that way
682
633
_no_decode_chars = ';/?:@&=+$,#'
683
634
_no_decode_ords = [ord(c) for c in _no_decode_chars]
684
635
_no_decode_hex = (['%02x' % o for o in _no_decode_ords]
685
 
                + ['%02X' % o for o in _no_decode_ords])
686
 
_hex_display_map = dict(([('%02x' % o, chr(o)) for o in range(256)]
687
 
                    + [('%02X' % o, chr(o)) for o in range(256)]))
688
 
#These entries get mapped to themselves
689
 
_hex_display_map.update((hex, '%'+hex) for hex in _no_decode_hex)
 
636
                  + ['%02X' % o for o in _no_decode_ords])
 
637
_hex_display_map = dict(([('%02x' % o, bytes([o])) for o in range(256)]
 
638
                         + [('%02X' % o, bytes([o])) for o in range(256)]))
 
639
# These entries get mapped to themselves
 
640
_hex_display_map.update((hex, b'%' + hex.encode('ascii'))
 
641
                        for hex in _no_decode_hex)
690
642
 
691
643
# These characters shouldn't be percent-encoded, and it's always safe to
692
644
# unencode them if they are.
693
645
_url_dont_escape_characters = set(
694
 
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
695
 
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
696
 
   "0123456789" # Numbers
697
 
   "-._~"  # Unreserved characters
 
646
    "abcdefghijklmnopqrstuvwxyz"  # Lowercase alpha
 
647
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # Uppercase alpha
 
648
    "0123456789"  # Numbers
 
649
    "-._~"  # Unreserved characters
698
650
)
699
651
 
700
652
# These characters should not be escaped
701
653
_url_safe_characters = set(
702
 
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
703
 
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
704
 
   "0123456789" # Numbers
705
 
   "_.-!~*'()"  # Unreserved characters
706
 
   "/;?:@&=+$," # Reserved characters
707
 
   "%#"         # Extra reserved characters
 
654
    "abcdefghijklmnopqrstuvwxyz"  # Lowercase alpha
 
655
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # Uppercase alpha
 
656
    "0123456789"  # Numbers
 
657
    "_.-!~*'()"  # Unreserved characters
 
658
    "/;?:@&=+$,"  # Reserved characters
 
659
    "%#"         # Extra reserved characters
708
660
)
709
661
 
 
662
 
 
663
def _unescape_segment_for_display(segment, encoding):
 
664
    """Unescape a segment for display.
 
665
 
 
666
    Helper for unescape_for_display
 
667
 
 
668
    :param url: A 7-bit ASCII URL
 
669
    :param encoding: The final output encoding
 
670
 
 
671
    :return: A unicode string which can be safely encoded into the
 
672
         specified encoding.
 
673
    """
 
674
    escaped_chunks = segment.split('%')
 
675
    escaped_chunks[0] = escaped_chunks[0].encode('utf-8')
 
676
    for j in range(1, len(escaped_chunks)):
 
677
        item = escaped_chunks[j]
 
678
        try:
 
679
            escaped_chunks[j] = _hex_display_map[item[:2]]
 
680
        except KeyError:
 
681
            # Put back the percent symbol
 
682
            escaped_chunks[j] = b'%' + (item[:2].encode('utf-8'))
 
683
        except UnicodeDecodeError:
 
684
            escaped_chunks[j] = chr(int(item[:2], 16)).encode('utf-8')
 
685
        escaped_chunks[j] += (item[2:].encode('utf-8'))
 
686
    unescaped = b''.join(escaped_chunks)
 
687
    try:
 
688
        decoded = unescaped.decode('utf-8')
 
689
    except UnicodeDecodeError:
 
690
        # If this path segment cannot be properly utf-8 decoded
 
691
        # after doing unescaping we will just leave it alone
 
692
        return segment
 
693
    else:
 
694
        try:
 
695
            decoded.encode(encoding)
 
696
        except UnicodeEncodeError:
 
697
            # If this chunk cannot be encoded in the local
 
698
            # encoding, then we should leave it alone
 
699
            return segment
 
700
        else:
 
701
            # Otherwise take the url decoded one
 
702
            return decoded
 
703
 
 
704
 
710
705
def unescape_for_display(url, encoding):
711
706
    """Decode what you can for a URL, so that we get a nice looking path.
712
707
 
735
730
    # Split into sections to try to decode utf-8
736
731
    res = url.split('/')
737
732
    for i in range(1, len(res)):
738
 
        escaped_chunks = res[i].split('%')
739
 
        for j in range(1, len(escaped_chunks)):
740
 
            item = escaped_chunks[j]
741
 
            try:
742
 
                escaped_chunks[j] = _hex_display_map[item[:2]] + item[2:]
743
 
            except KeyError:
744
 
                # Put back the percent symbol
745
 
                escaped_chunks[j] = '%' + item
746
 
            except UnicodeDecodeError:
747
 
                escaped_chunks[j] = unichr(int(item[:2], 16)) + item[2:]
748
 
        unescaped = ''.join(escaped_chunks)
749
 
        try:
750
 
            decoded = unescaped.decode('utf-8')
751
 
        except UnicodeDecodeError:
752
 
            # If this path segment cannot be properly utf-8 decoded
753
 
            # after doing unescaping we will just leave it alone
754
 
            pass
755
 
        else:
756
 
            try:
757
 
                decoded.encode(encoding)
758
 
            except UnicodeEncodeError:
759
 
                # If this chunk cannot be encoded in the local
760
 
                # encoding, then we should leave it alone
761
 
                pass
762
 
            else:
763
 
                # Otherwise take the url decoded one
764
 
                res[i] = decoded
 
733
        res[i] = _unescape_segment_for_display(res[i], encoding)
765
734
    return u'/'.join(res)
766
735
 
767
736
 
776
745
    is used without a path, e.g. c:foo-bar => foo-bar.
777
746
    If no /, path separator or : is found, the from_location is returned.
778
747
    """
 
748
    from_location = strip_segment_parameters(from_location)
779
749
    if from_location.find("/") >= 0 or from_location.find(os.sep) >= 0:
780
750
        return os.path.basename(from_location.rstrip("/\\"))
781
751
    else:
782
752
        sep = from_location.find(":")
783
753
        if sep > 0:
784
 
            return from_location[sep+1:]
 
754
            return from_location[sep + 1:]
785
755
        else:
786
756
            return from_location
787
757
 
815
785
    to_segments = osutils.splitpath(to_path)
816
786
    count = -1
817
787
    for count, (from_element, to_element) in enumerate(zip(from_segments,
818
 
                                                       to_segments)):
 
788
                                                           to_segments)):
819
789
        if from_element != to_element:
820
790
            break
821
791
    else:
832
802
    """Parsed URL."""
833
803
 
834
804
    def __init__(self, scheme, quoted_user, quoted_password, quoted_host,
835
 
            port, quoted_path):
 
805
                 port, quoted_path):
836
806
        self.scheme = scheme
837
807
        self.quoted_host = quoted_host
838
808
        self.host = unquote(self.quoted_host)
847
817
        else:
848
818
            self.password = None
849
819
        self.port = port
850
 
        self.quoted_path = _url_hex_escapes_re.sub(_unescape_safe_chars, quoted_path)
 
820
        self.quoted_path = _url_hex_escapes_re.sub(
 
821
            _unescape_safe_chars, quoted_path)
851
822
        self.path = unquote(self.quoted_path)
852
823
 
853
824
    def __eq__(self, other):
871
842
        :param url: URL as bytestring
872
843
        """
873
844
        # GZ 2017-06-09: Actually validate ascii-ness
874
 
        if not isinstance(url, str):
875
 
            raise InvalidURL('should be ascii:\n%r' % url)
 
845
        # pad.lv/1696545: For the moment, accept both native strings and
 
846
        # unicode.
 
847
        if isinstance(url, str):
 
848
            pass
 
849
        elif isinstance(url, str):
 
850
            try:
 
851
                url = url.encode()
 
852
            except UnicodeEncodeError:
 
853
                raise InvalidURL(url)
 
854
        else:
 
855
            raise InvalidURL(url)
876
856
        (scheme, netloc, path, params,
877
857
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
878
858
        user = password = host = port = None
886
866
        if ':' in host and not (host[0] == '[' and host[-1] == ']'):
887
867
            # there *is* port
888
868
            host, port = host.rsplit(':', 1)
889
 
            try:
890
 
                port = int(port)
891
 
            except ValueError:
892
 
                raise InvalidURL('invalid port number %s in url:\n%s' %
893
 
                                 (port, url))
894
 
        if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
 
869
            if port:
 
870
                try:
 
871
                    port = int(port)
 
872
                except ValueError:
 
873
                    raise InvalidURL('invalid port number %s in url:\n%s' %
 
874
                                     (port, url))
 
875
            else:
 
876
                port = None
 
877
        if host != "" and host[0] == '[' and host[-1] == ']':  # IPv6
895
878
            host = host[1:-1]
896
879
 
897
880
        return cls(scheme, user, password, host, port, path)
930
913
        :param relpath: relative url string for relative part of remote path.
931
914
        :return: urlencoded string for final path.
932
915
        """
933
 
        if not isinstance(relpath, str):
 
916
        # pad.lv/1696545: For the moment, accept both native strings and
 
917
        # unicode.
 
918
        if isinstance(relpath, str):
 
919
            pass
 
920
        elif isinstance(relpath, str):
 
921
            try:
 
922
                relpath = relpath.encode()
 
923
            except UnicodeEncodeError:
 
924
                raise InvalidURL(relpath)
 
925
        else:
934
926
            raise InvalidURL(relpath)
935
927
        relpath = _url_hex_escapes_re.sub(_unescape_safe_chars, relpath)
936
928
        if relpath.startswith('/'):
947
939
                    continue
948
940
                base_parts.pop()
949
941
            elif p == '.':
950
 
                continue # No-op
 
942
                continue  # No-op
951
943
            elif p != '':
952
944
                base_parts.append(p)
953
945
        path = '/'.join(base_parts)
962
954
        :return: `URL` instance
963
955
        """
964
956
        if offset is not None:
965
 
            relative = unescape(offset).encode('utf-8')
 
957
            relative = unescape(offset)
966
958
            path = self._combine_paths(self.path, relative)
967
959
            path = quote(path, safe="/~")
968
960
        else:
969
961
            path = self.quoted_path
970
962
        return self.__class__(self.scheme, self.quoted_user,
971
 
                self.quoted_password, self.quoted_host, self.port,
972
 
                path)
 
963
                              self.quoted_password, self.quoted_host, self.port,
 
964
                              path)
973
965
 
974
966
 
975
967
def parse_url(url):
984
976
    """
985
977
    parsed_url = URL.from_string(url)
986
978
    return (parsed_url.scheme, parsed_url.user, parsed_url.password,
987
 
        parsed_url.host, parsed_url.port, parsed_url.path)
 
979
            parsed_url.host, parsed_url.port, parsed_url.path)