Skip to content

Commit

Permalink
Merge branch 'master' into black
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wsanchez committed Apr 2, 2020
2 parents 20ece13 + 02af58e commit 5bd8b7b
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 38 deletions.
13 changes: 7 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
/docs/_build
tmp.py
/htmlcov/
/htmldocs/
.coverage.*
*.py[cod]
.mypy_cache

# emacs
*~
Expand Down Expand Up @@ -33,11 +29,16 @@ lib64
# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
# Testing
/.tox/
nosetests.xml

# Coverage
/.coverage
/.coverage.*
/htmlcov/
/.mypy_cache

# Translations
*.mo

Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ matrix:
- python: "3.8"
env: TOXENV=test-py38,codecov
- python: "pypy"
env: TOXENV=test-pypy,codecov
env: TOXENV=test-pypy2,codecov
- python: "pypy3"
env: TOXENV=test-pypy3,codecov
- python: "2.7"
Expand Down
58 changes: 48 additions & 10 deletions src/hyperlink/_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -815,9 +815,18 @@ class URL(object):
that starts with a slash.
userinfo (Text): The username or colon-separated
username:password pair.
uses_netloc (bool): Indicates whether two slashes appear
between the scheme and the host (``http://eg.com`` vs
``mailto:[email protected]``). Set automatically based on scheme.
uses_netloc (Optional[bool]): Indicates whether ``://`` (the "netloc
separator") will appear to separate the scheme from the *path* in
cases where no host is present. Setting this to ``True`` is a
non-spec-compliant affordance for the common practice of having URIs
that are *not* URLs (cannot have a 'host' part) but nevertheless use
the common ``://`` idiom that most people associate with URLs;
e.g. ``message:`` URIs like ``message://message-id`` being
equivalent to ``message:message-id``. This may be inferred based on
the scheme depending on whether :func:`register_scheme` has been
used to register the scheme and should not be passed directly unless
you know the scheme works like this and you know it has not been
registered.
All of these parts are also exposed as read-only attributes of
URL instances, along with several useful methods.
Expand Down Expand Up @@ -882,15 +891,28 @@ def __init__(
self._rooted = _typecheck("rooted", rooted, bool)
self._userinfo = _textcheck("userinfo", userinfo, '/?#@')

uses_netloc = scheme_uses_netloc(self._scheme, uses_netloc)
if uses_netloc is None:
uses_netloc = scheme_uses_netloc(self._scheme, uses_netloc)
self._uses_netloc = _typecheck("uses_netloc",
uses_netloc, bool, NoneType)
# fixup for rooted consistency
if self._host:
will_have_authority = (
self._host or
(self._port and self._port != SCHEME_PORT_MAP.get(scheme))
)
if will_have_authority:
# fixup for rooted consistency; if there's any 'authority'
# represented in the textual URL, then the path must be rooted, and
# we're definitely using a netloc (there must be a ://).
self._rooted = True
if (not self._rooted) and self._path and self._path[0] == '':
self._uses_netloc = True
if (not self._rooted) and self.path[:1] == (u'',):
self._rooted = True
self._path = self._path[1:]
if not will_have_authority and self._path and not self._rooted:
# If, after fixing up the path, there *is* a path and it *isn't*
# rooted, then we are definitely not using a netloc; if we did, it
# would make the path (erroneously) look like a hostname.
self._uses_netloc = False

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

Expand Down Expand Up @@ -1134,14 +1158,28 @@ def replace(
slash.
userinfo (Text): The username or colon-separated username:password
pair.
uses_netloc (bool): Indicates whether two slashes appear between
the scheme and the host
(``http://eg.com`` vs ``mailto:[email protected]``)
uses_netloc (bool): Indicates whether ``://`` (the "netloc
separator") will appear to separate the scheme from the *path*
in cases where no host is present. Setting this to ``True`` is
a non-spec-compliant affordance for the common practice of
having URIs that are *not* URLs (cannot have a 'host' part) but
nevertheless use the common ``://`` idiom that most people
associate with URLs; e.g. ``message:`` URIs like
``message://message-id`` being equivalent to
``message:message-id``. This may be inferred based on the
scheme depending on whether :func:`register_scheme` has been
used to register the scheme and should not be passed directly
unless you know the scheme works like this and you know it has
not been registered.
Returns:
URL: A copy of the current :class:`URL`, with new values for
parameters passed.
"""
if scheme is not _UNSET and scheme != self.scheme:
# when changing schemes, reset the explicit uses_netloc preference
# to honor the new scheme.
uses_netloc = None
return self.__class__(
scheme=_optional(scheme, self.scheme),
host=_optional(host, self.host),
Expand Down
54 changes: 54 additions & 0 deletions src/hyperlink/test/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,18 @@ def test_mailto(self):
self.assertEqual(URL.from_text(u"mailto:[email protected]").to_text(),
u"mailto:[email protected]")

def test_httpWithoutHost(self):
# type: () -> None
"""
An HTTP URL without a hostname, but with a path, should also round-trip
cleanly.
"""
without_host = URL.from_text(u"http:relative-path")
self.assertEqual(without_host.host, u'')
self.assertEqual(without_host.path, (u'relative-path',))
self.assertEqual(without_host.uses_netloc, False)
self.assertEqual(without_host.to_text(), u"http:relative-path")

def test_queryIterable(self):
# type: () -> None
"""
Expand Down Expand Up @@ -938,15 +950,29 @@ def test_netloc(self):
# type: () -> None
url = URL(scheme='https')
self.assertEqual(url.uses_netloc, True)
self.assertEqual(url.to_text(), u'https://')
# scheme, no host, no path, no netloc hack
self.assertEqual(URL.from_text('https:').uses_netloc, False)
# scheme, no host, absolute path, no netloc hack
self.assertEqual(URL.from_text('https:/').uses_netloc, False)
# scheme, no host, no path, netloc hack to indicate :// syntax
self.assertEqual(URL.from_text('https://').uses_netloc, True)

url = URL(scheme='https', uses_netloc=False)
self.assertEqual(url.uses_netloc, False)
self.assertEqual(url.to_text(), u'https:')

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

url = URL(scheme='mailto')
self.assertEqual(url.uses_netloc, False)
self.assertEqual(url.to_text(), u'mailto:')

url = URL(scheme='ztp')
self.assertEqual(url.uses_netloc, None)
self.assertEqual(url.to_text(), u'ztp:')

url = URL.from_text('ztp://test.com')
self.assertEqual(url.uses_netloc, True)
Expand Down Expand Up @@ -1116,6 +1142,34 @@ def test_autorooted(self):
self.assertEqual(normal_absolute.rooted, True)
self.assertEqual(attempt_unrooted_absolute.rooted, True)

def test_rooted_with_port_but_no_host(self):
# type: () -> None
"""
URLs which include a ``://`` netloc-separator for any reason are
inherently rooted, regardless of the value or presence of the
``rooted`` constructor argument.
They may include a netloc-separator because their constructor was
directly invoked with an explicit host or port, or because they were
parsed from a string which included the literal ``://`` separator.
"""
directly_constructed = URL(scheme='udp', port=4900, rooted=False)
directly_constructed_implict = URL(scheme='udp', port=4900)
directly_constructed_rooted = URL(scheme=u'udp', port=4900,
rooted=True)
self.assertEqual(directly_constructed.rooted, True)
self.assertEqual(directly_constructed_implict.rooted, True)
self.assertEqual(directly_constructed_rooted.rooted, True)
parsed = URL.from_text('udp://:4900')
self.assertEqual(str(directly_constructed), str(parsed))
self.assertEqual(str(directly_constructed_implict), str(parsed))
self.assertEqual(directly_constructed.asText(), parsed.asText())
self.assertEqual(directly_constructed, parsed)
self.assertEqual(directly_constructed, directly_constructed_implict)
self.assertEqual(directly_constructed, directly_constructed_rooted)
self.assertEqual(directly_constructed_implict, parsed)
self.assertEqual(directly_constructed_rooted, parsed)

def test_wrong_constructor(self):
# type: () -> None
with self.assertRaises(ValueError):
Expand Down
38 changes: 17 additions & 21 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

envlist =
flake8, mypy # black
test-py{26,27,34,35,36,37,38,py,py3}
test-py{26,27,34,35,36,37,38,py2,py3}
coverage_report
docs
packaging
Expand All @@ -14,6 +14,15 @@ skip_missing_interpreters = {tty:True:False}

basepython = python3.8

deps =
idna==2.9

test: typing==3.7.4.1
test: {[testenv:coverage_report]deps}
test-{py26,py27,py34}: pytest==4.6.9
test-{py35,py36,py37,py38}: pytest==5.2.4
test: pytest-cov==2.8.1

setenv =
PY_MODULE=hyperlink

Expand All @@ -37,20 +46,11 @@ basepython =
py37: python3.7
py38: python3.8
py39: python3.9
pypy: pypy
pypy3: pypy3

deps =
test: coverage==4.5.4 # rq.filter: <5
test: idna==2.9
test: typing==3.7.4.1
test: {py26,py27,py34}: pytest==4.6.9
test: {py35,py36,py37,py38}: pytest==5.2.4
test: pytest-cov==2.8.1
test: {[testenv:coverage_report]deps}
pypy2: pypy
pypy3: pypy3

passenv =
test: CI
deps = {[default]deps}

setenv =
{[default]setenv}
Expand Down Expand Up @@ -174,6 +174,8 @@ basepython = {[default]basepython}
deps =
mypy==0.770

{[default]deps}

commands =
mypy \
--config-file="{toxinidir}/tox.ini" \
Expand All @@ -200,11 +202,7 @@ warn_return_any = True
warn_unreachable = True
warn_unused_ignores = True

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


[mypy-idna]
ignore_missing_imports = True
Expand All @@ -218,7 +216,7 @@ ignore_missing_imports = True

description = generate coverage report

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

basepython = {[default]basepython}

Expand Down Expand Up @@ -254,7 +252,6 @@ skip_install = True

deps =
{[testenv:coverage_report]deps}

codecov==2.0.22

passenv =
Expand Down Expand Up @@ -297,7 +294,7 @@ description = build documentation
basepython = {[default]basepython}

deps =
Sphinx==2.3.1
Sphinx==2.4.4
sphinx-rtd-theme==0.4.3

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

deps =
{[testenv:docs]deps}

sphinx-autobuild==0.7.1

commands =
Expand Down

0 comments on commit 5bd8b7b

Please sign in to comment.