Skip to content

Commit 32b078b

Browse files
committed
fix up inconsistencies in parsing & textual representation of 'rooted' and 'uses_netloc'
1 parent 688233a commit 32b078b

2 files changed

Lines changed: 62 additions & 10 deletions

File tree

src/hyperlink/_url.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -815,9 +815,9 @@ class URL(object):
815815
that starts with a slash.
816816
userinfo (Text): The username or colon-separated
817817
username:password pair.
818-
uses_netloc (bool): Indicates whether two slashes appear
819-
between the scheme and the host (``http://eg.com`` vs
820-
``mailto:e@g.com``). Set automatically based on scheme.
818+
uses_netloc (bool): Indicates whether ``://`` will appear to separate
819+
the scheme from the path, even in cases where no host is present.
820+
May be implied by scheme, or set explictly.
821821
822822
All of these parts are also exposed as read-only attributes of
823823
URL instances, along with several useful methods.
@@ -882,15 +882,28 @@ def __init__(
882882
self._rooted = _typecheck("rooted", rooted, bool)
883883
self._userinfo = _textcheck("userinfo", userinfo, '/?#@')
884884

885-
uses_netloc = scheme_uses_netloc(self._scheme, uses_netloc)
885+
if uses_netloc is None:
886+
uses_netloc = scheme_uses_netloc(self._scheme, uses_netloc)
886887
self._uses_netloc = _typecheck("uses_netloc",
887888
uses_netloc, bool, NoneType)
888-
# fixup for rooted consistency
889-
if self._host:
889+
will_have_authority = (
890+
self._host or
891+
(self._port and self._port != SCHEME_PORT_MAP.get(scheme))
892+
)
893+
if will_have_authority:
894+
# fixup for rooted consistency; if there's any 'authority'
895+
# represented in the textual URL, then the path must be rooted, and
896+
# we're definitely using a netloc (there must be a ://).
890897
self._rooted = True
891-
if (not self._rooted) and self._path and self._path[0] == '':
898+
self._uses_netloc = True
899+
if (not self._rooted) and self.path[:1] == (u'',):
892900
self._rooted = True
893901
self._path = self._path[1:]
902+
if not will_have_authority and self._path and not self._rooted:
903+
# If, after fixing up the path, there *is* a path and it *isn't*
904+
# rooted, then we are definitely not using a netloc; if we did, it
905+
# would make the path (erroneously) look like a hostname.
906+
self._uses_netloc = False
894907

895908
def get_decoded_url(self, lazy=False):
896909
# type: (bool) -> DecodedURL
@@ -1006,6 +1019,8 @@ def userinfo(self):
10061019
def uses_netloc(self):
10071020
# type: () -> Optional[bool]
10081021
"""
1022+
Whether the textual URL representation will contain a ``://`` netloc
1023+
separator.
10091024
"""
10101025
return self._uses_netloc
10111026

@@ -1134,14 +1149,17 @@ def replace(
11341149
slash.
11351150
userinfo (Text): The username or colon-separated username:password
11361151
pair.
1137-
uses_netloc (bool): Indicates whether two slashes appear between
1138-
the scheme and the host
1139-
(``http://eg.com`` vs ``mailto:e@g.com``)
1152+
uses_netloc (bool): Indicates whether rooted paths should include a
1153+
scheme-separator by default.
11401154
11411155
Returns:
11421156
URL: A copy of the current :class:`URL`, with new values for
11431157
parameters passed.
11441158
"""
1159+
if scheme is not _UNSET and scheme != self.scheme:
1160+
# when changing schemes, reset the explicit uses_netloc preference
1161+
# to honor the new scheme.
1162+
uses_netloc = None
11451163
return self.__class__(
11461164
scheme=_optional(scheme, self.scheme),
11471165
host=_optional(host, self.host),

src/hyperlink/test/test_url.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,18 @@ def test_mailto(self):
817817
self.assertEqual(URL.from_text(u"mailto:user@example.com").to_text(),
818818
u"mailto:user@example.com")
819819

820+
def test_httpWithoutHost(self):
821+
# type: () -> None
822+
"""
823+
An HTTP URL without a hostname, but with a path, should also round-trip
824+
cleanly.
825+
"""
826+
without_host = URL.from_text(u"http:relative-path")
827+
self.assertEqual(without_host.host, u'')
828+
self.assertEqual(without_host.path, (u'relative-path',))
829+
self.assertEqual(without_host.uses_netloc, False)
830+
self.assertEqual(without_host.to_text(), u"http:relative-path")
831+
820832
def test_queryIterable(self):
821833
# type: () -> None
822834
"""
@@ -938,15 +950,25 @@ def test_netloc(self):
938950
# type: () -> None
939951
url = URL(scheme='https')
940952
self.assertEqual(url.uses_netloc, True)
953+
self.assertEqual(url.to_text(), u'https://')
954+
self.assertEqual(URL.from_text('https:').uses_netloc, False)
955+
self.assertEqual(URL.from_text('https://').uses_netloc, True)
956+
957+
url = URL(scheme='https', uses_netloc=False)
958+
self.assertEqual(url.uses_netloc, False)
959+
self.assertEqual(url.to_text(), u'https:')
941960

942961
url = URL(scheme='git+https')
943962
self.assertEqual(url.uses_netloc, True)
963+
self.assertEqual(url.to_text(), u'git+https://')
944964

945965
url = URL(scheme='mailto')
946966
self.assertEqual(url.uses_netloc, False)
967+
self.assertEqual(url.to_text(), u'mailto:')
947968

948969
url = URL(scheme='ztp')
949970
self.assertEqual(url.uses_netloc, None)
971+
self.assertEqual(url.to_text(), u'ztp:')
950972

951973
url = URL.from_text('ztp://test.com')
952974
self.assertEqual(url.uses_netloc, True)
@@ -1116,6 +1138,18 @@ def test_autorooted(self):
11161138
self.assertEqual(normal_absolute.rooted, True)
11171139
self.assertEqual(attempt_unrooted_absolute.rooted, True)
11181140

1141+
def test_rooted_with_empty_non_none_host(self):
1142+
# type: () -> None
1143+
"""
1144+
The C{rooted} constructor argument will be ignored on URLs that include
1145+
a scheme.
1146+
"""
1147+
directly_constructed = URL(scheme='udp', port=4900)
1148+
parsed = URL.from_text('udp://:4900')
1149+
self.assertEqual(str(directly_constructed), str(parsed))
1150+
self.assertEqual(directly_constructed.asText(), parsed.asText())
1151+
self.assertEqual(directly_constructed, parsed)
1152+
11191153
def test_wrong_constructor(self):
11201154
# type: () -> None
11211155
with self.assertRaises(ValueError):

0 commit comments

Comments
 (0)