Skip to content

Commit 5bd8b7b

Browse files
committed
Merge branch 'master' into black
* master: [requires.io] dependency update [requires.io] dependency update Spiff up the tox config a bit more There is no requirements-test.txt file in the source tree. Archor a few paths to the root of the source tree. Minor re-ordering. per CR: rephrase gibberish test docstring per CR: add https:/, enumerate the cases per CR: match __init__ match __init__ doc per CR: explain in much more detail <79 per CR: make the test a little more thorough, improve docstring fix up inconsistencies in parsing & textual representation of 'rooted' and 'uses_netloc' # Conflicts: # .gitignore # MANIFEST.in # tox.ini
2 parents 20ece13 + 02af58e commit 5bd8b7b

5 files changed

Lines changed: 127 additions & 38 deletions

File tree

.gitignore

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
/docs/_build
22
tmp.py
3-
/htmlcov/
4-
/htmldocs/
5-
.coverage.*
63
*.py[cod]
7-
.mypy_cache
84

95
# emacs
106
*~
@@ -33,11 +29,16 @@ lib64
3329
# Installer logs
3430
pip-log.txt
3531

36-
# Unit test / coverage reports
37-
.coverage
32+
# Testing
3833
/.tox/
3934
nosetests.xml
4035

36+
# Coverage
37+
/.coverage
38+
/.coverage.*
39+
/htmlcov/
40+
/.mypy_cache
41+
4142
# Translations
4243
*.mo
4344

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ matrix:
2222
- python: "3.8"
2323
env: TOXENV=test-py38,codecov
2424
- python: "pypy"
25-
env: TOXENV=test-pypy,codecov
25+
env: TOXENV=test-pypy2,codecov
2626
- python: "pypy3"
2727
env: TOXENV=test-pypy3,codecov
2828
- python: "2.7"

src/hyperlink/_url.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -815,9 +815,18 @@ 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 (Optional[bool]): Indicates whether ``://`` (the "netloc
819+
separator") will appear to separate the scheme from the *path* in
820+
cases where no host is present. Setting this to ``True`` is a
821+
non-spec-compliant affordance for the common practice of having URIs
822+
that are *not* URLs (cannot have a 'host' part) but nevertheless use
823+
the common ``://`` idiom that most people associate with URLs;
824+
e.g. ``message:`` URIs like ``message://message-id`` being
825+
equivalent to ``message:message-id``. This may be inferred based on
826+
the scheme depending on whether :func:`register_scheme` has been
827+
used to register the scheme and should not be passed directly unless
828+
you know the scheme works like this and you know it has not been
829+
registered.
821830
822831
All of these parts are also exposed as read-only attributes of
823832
URL instances, along with several useful methods.
@@ -882,15 +891,28 @@ def __init__(
882891
self._rooted = _typecheck("rooted", rooted, bool)
883892
self._userinfo = _textcheck("userinfo", userinfo, '/?#@')
884893

885-
uses_netloc = scheme_uses_netloc(self._scheme, uses_netloc)
894+
if uses_netloc is None:
895+
uses_netloc = scheme_uses_netloc(self._scheme, uses_netloc)
886896
self._uses_netloc = _typecheck("uses_netloc",
887897
uses_netloc, bool, NoneType)
888-
# fixup for rooted consistency
889-
if self._host:
898+
will_have_authority = (
899+
self._host or
900+
(self._port and self._port != SCHEME_PORT_MAP.get(scheme))
901+
)
902+
if will_have_authority:
903+
# fixup for rooted consistency; if there's any 'authority'
904+
# represented in the textual URL, then the path must be rooted, and
905+
# we're definitely using a netloc (there must be a ://).
890906
self._rooted = True
891-
if (not self._rooted) and self._path and self._path[0] == '':
907+
self._uses_netloc = True
908+
if (not self._rooted) and self.path[:1] == (u'',):
892909
self._rooted = True
893910
self._path = self._path[1:]
911+
if not will_have_authority and self._path and not self._rooted:
912+
# If, after fixing up the path, there *is* a path and it *isn't*
913+
# rooted, then we are definitely not using a netloc; if we did, it
914+
# would make the path (erroneously) look like a hostname.
915+
self._uses_netloc = False
894916

895917
def get_decoded_url(self, lazy=False):
896918
# type: (bool) -> DecodedURL
@@ -1006,6 +1028,8 @@ def userinfo(self):
10061028
def uses_netloc(self):
10071029
# type: () -> Optional[bool]
10081030
"""
1031+
Indicates whether ``://`` (the "netloc separator") will appear to
1032+
separate the scheme from the *path* in cases where no host is present.
10091033
"""
10101034
return self._uses_netloc
10111035

@@ -1134,14 +1158,28 @@ def replace(
11341158
slash.
11351159
userinfo (Text): The username or colon-separated username:password
11361160
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``)
1161+
uses_netloc (bool): Indicates whether ``://`` (the "netloc
1162+
separator") will appear to separate the scheme from the *path*
1163+
in cases where no host is present. Setting this to ``True`` is
1164+
a non-spec-compliant affordance for the common practice of
1165+
having URIs that are *not* URLs (cannot have a 'host' part) but
1166+
nevertheless use the common ``://`` idiom that most people
1167+
associate with URLs; e.g. ``message:`` URIs like
1168+
``message://message-id`` being equivalent to
1169+
``message:message-id``. This may be inferred based on the
1170+
scheme depending on whether :func:`register_scheme` has been
1171+
used to register the scheme and should not be passed directly
1172+
unless you know the scheme works like this and you know it has
1173+
not been registered.
11401174
11411175
Returns:
11421176
URL: A copy of the current :class:`URL`, with new values for
11431177
parameters passed.
11441178
"""
1179+
if scheme is not _UNSET and scheme != self.scheme:
1180+
# when changing schemes, reset the explicit uses_netloc preference
1181+
# to honor the new scheme.
1182+
uses_netloc = None
11451183
return self.__class__(
11461184
scheme=_optional(scheme, self.scheme),
11471185
host=_optional(host, self.host),

src/hyperlink/test/test_url.py

Lines changed: 54 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,29 @@ 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+
# scheme, no host, no path, no netloc hack
955+
self.assertEqual(URL.from_text('https:').uses_netloc, False)
956+
# scheme, no host, absolute path, no netloc hack
957+
self.assertEqual(URL.from_text('https:/').uses_netloc, False)
958+
# scheme, no host, no path, netloc hack to indicate :// syntax
959+
self.assertEqual(URL.from_text('https://').uses_netloc, True)
960+
961+
url = URL(scheme='https', uses_netloc=False)
962+
self.assertEqual(url.uses_netloc, False)
963+
self.assertEqual(url.to_text(), u'https:')
941964

942965
url = URL(scheme='git+https')
943966
self.assertEqual(url.uses_netloc, True)
967+
self.assertEqual(url.to_text(), u'git+https://')
944968

945969
url = URL(scheme='mailto')
946970
self.assertEqual(url.uses_netloc, False)
971+
self.assertEqual(url.to_text(), u'mailto:')
947972

948973
url = URL(scheme='ztp')
949974
self.assertEqual(url.uses_netloc, None)
975+
self.assertEqual(url.to_text(), u'ztp:')
950976

951977
url = URL.from_text('ztp://test.com')
952978
self.assertEqual(url.uses_netloc, True)
@@ -1116,6 +1142,34 @@ def test_autorooted(self):
11161142
self.assertEqual(normal_absolute.rooted, True)
11171143
self.assertEqual(attempt_unrooted_absolute.rooted, True)
11181144

1145+
def test_rooted_with_port_but_no_host(self):
1146+
# type: () -> None
1147+
"""
1148+
URLs which include a ``://`` netloc-separator for any reason are
1149+
inherently rooted, regardless of the value or presence of the
1150+
``rooted`` constructor argument.
1151+
1152+
They may include a netloc-separator because their constructor was
1153+
directly invoked with an explicit host or port, or because they were
1154+
parsed from a string which included the literal ``://`` separator.
1155+
"""
1156+
directly_constructed = URL(scheme='udp', port=4900, rooted=False)
1157+
directly_constructed_implict = URL(scheme='udp', port=4900)
1158+
directly_constructed_rooted = URL(scheme=u'udp', port=4900,
1159+
rooted=True)
1160+
self.assertEqual(directly_constructed.rooted, True)
1161+
self.assertEqual(directly_constructed_implict.rooted, True)
1162+
self.assertEqual(directly_constructed_rooted.rooted, True)
1163+
parsed = URL.from_text('udp://:4900')
1164+
self.assertEqual(str(directly_constructed), str(parsed))
1165+
self.assertEqual(str(directly_constructed_implict), str(parsed))
1166+
self.assertEqual(directly_constructed.asText(), parsed.asText())
1167+
self.assertEqual(directly_constructed, parsed)
1168+
self.assertEqual(directly_constructed, directly_constructed_implict)
1169+
self.assertEqual(directly_constructed, directly_constructed_rooted)
1170+
self.assertEqual(directly_constructed_implict, parsed)
1171+
self.assertEqual(directly_constructed_rooted, parsed)
1172+
11191173
def test_wrong_constructor(self):
11201174
# type: () -> None
11211175
with self.assertRaises(ValueError):

tox.ini

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
envlist =
44
flake8, mypy # black
5-
test-py{26,27,34,35,36,37,38,py,py3}
5+
test-py{26,27,34,35,36,37,38,py2,py3}
66
coverage_report
77
docs
88
packaging
@@ -14,6 +14,15 @@ skip_missing_interpreters = {tty:True:False}
1414

1515
basepython = python3.8
1616

17+
deps =
18+
idna==2.9
19+
20+
test: typing==3.7.4.1
21+
test: {[testenv:coverage_report]deps}
22+
test-{py26,py27,py34}: pytest==4.6.9
23+
test-{py35,py36,py37,py38}: pytest==5.2.4
24+
test: pytest-cov==2.8.1
25+
1726
setenv =
1827
PY_MODULE=hyperlink
1928

@@ -37,20 +46,11 @@ basepython =
3746
py37: python3.7
3847
py38: python3.8
3948
py39: python3.9
40-
pypy: pypy
41-
pypy3: pypy3
4249

43-
deps =
44-
test: coverage==4.5.4 # rq.filter: <5
45-
test: idna==2.9
46-
test: typing==3.7.4.1
47-
test: {py26,py27,py34}: pytest==4.6.9
48-
test: {py35,py36,py37,py38}: pytest==5.2.4
49-
test: pytest-cov==2.8.1
50-
test: {[testenv:coverage_report]deps}
50+
pypy2: pypy
51+
pypy3: pypy3
5152

52-
passenv =
53-
test: CI
53+
deps = {[default]deps}
5454

5555
setenv =
5656
{[default]setenv}
@@ -174,6 +174,8 @@ basepython = {[default]basepython}
174174
deps =
175175
mypy==0.770
176176

177+
{[default]deps}
178+
177179
commands =
178180
mypy \
179181
--config-file="{toxinidir}/tox.ini" \
@@ -200,11 +202,7 @@ warn_return_any = True
200202
warn_unreachable = True
201203
warn_unused_ignores = True
202204

203-
[mypy-hyperlink._url]
204205
# Don't complain about dependencies known to lack type hints
205-
# 4 at time of writing (2020-20-01), so maybe disable this soon
206-
allow_untyped_defs = True
207-
208206

209207
[mypy-idna]
210208
ignore_missing_imports = True
@@ -218,7 +216,7 @@ ignore_missing_imports = True
218216

219217
description = generate coverage report
220218

221-
depends = test-py{36,37,38,39,py3}
219+
depends = test-py{26,27,34,35,36,37,38,py,py3}
222220

223221
basepython = {[default]basepython}
224222

@@ -254,7 +252,6 @@ skip_install = True
254252

255253
deps =
256254
{[testenv:coverage_report]deps}
257-
258255
codecov==2.0.22
259256

260257
passenv =
@@ -297,7 +294,7 @@ description = build documentation
297294
basepython = {[default]basepython}
298295

299296
deps =
300-
Sphinx==2.3.1
297+
Sphinx==2.4.4
301298
sphinx-rtd-theme==0.4.3
302299

303300
commands =
@@ -315,7 +312,6 @@ basepython = {[default]basepython}
315312

316313
deps =
317314
{[testenv:docs]deps}
318-
319315
sphinx-autobuild==0.7.1
320316

321317
commands =

0 commit comments

Comments
 (0)