Merge branch 'master' into strategies
* master: (24 commits) Remove W504 Add Glyph's text for #112. Note changes since 19.0.0 Apply black to setup.py also Run black-reformat Couple more cleanup bits Ignore /htmldocs [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
This commit is contained in:
commit
1b1e12a62e
|
@ -1,9 +1,6 @@
|
|||
docs/_build
|
||||
/docs/_build/
|
||||
tmp.py
|
||||
htmlcov/
|
||||
.coverage.*
|
||||
*.py[cod]
|
||||
/.hypothesis/
|
||||
|
||||
# emacs
|
||||
*~
|
||||
|
@ -32,11 +29,23 @@ lib64
|
|||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox/
|
||||
# Testing
|
||||
/.tox/
|
||||
/.hypothesis/
|
||||
nosetests.xml
|
||||
|
||||
# Coverage
|
||||
/.coverage
|
||||
/.coverage.*
|
||||
/htmlcov/
|
||||
/.mypy_cache/
|
||||
|
||||
# Documentation
|
||||
/htmldocs/
|
||||
|
||||
# Documentation
|
||||
/htmldocs/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -2,6 +2,15 @@
|
|||
|
||||
## dev (not yet released)
|
||||
|
||||
* CPython 3.7 and 3.8 and PyPy3 added to test matrix
|
||||
* Hyperlink now has type hints and they are now exported per
|
||||
[PEP 561](https://www.python.org/dev/peps/pep-0561/).
|
||||
* Several bugs related to hidden state were fixed, making it so that all data
|
||||
on a `URL` object (including `rooted` and `uses_netloc`) is reflected by and
|
||||
consistent with its textual representation.
|
||||
This does mean that sometimes these constructor arguments are ignored, if it
|
||||
would create invalid or unparseable URL text.
|
||||
|
||||
## 19.0.0
|
||||
|
||||
*(April 7, 2019)*
|
||||
|
@ -13,7 +22,8 @@ A query parameter-centric release, with two enhancements:
|
|||
[#39](https://github.com/python-hyper/hyperlink/pull/39))
|
||||
* `URL.remove()` now accepts *value* and *limit* parameters, allowing
|
||||
for removal of specific name-value pairs, as well as limiting the
|
||||
number of removals. (see [#71](https://github.com/python-hyper/hyperlink/pull/71))
|
||||
number of removals.
|
||||
(See [#71](https://github.com/python-hyper/hyperlink/pull/71))
|
||||
|
||||
## 18.0.0
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
[build-system]
|
||||
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
|
||||
[tool.black]
|
||||
|
||||
line-length = 80
|
||||
target-version = ["py27"]
|
83
setup.py
83
setup.py
|
@ -10,52 +10,47 @@ See the docs at http://hyperlink.readthedocs.io.
|
|||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
__author__ = 'Mahmoud Hashemi and Glyph Lefkowitz'
|
||||
__version__ = '19.0.1dev'
|
||||
__contact__ = 'mahmoud@hatnote.com'
|
||||
__url__ = 'https://github.com/python-hyper/hyperlink'
|
||||
__license__ = 'MIT'
|
||||
__author__ = "Mahmoud Hashemi and Glyph Lefkowitz"
|
||||
__version__ = "19.0.1dev"
|
||||
__contact__ = "mahmoud@hatnote.com"
|
||||
__url__ = "https://github.com/python-hyper/hyperlink"
|
||||
__license__ = "MIT"
|
||||
|
||||
|
||||
setup(name='hyperlink',
|
||||
version=__version__,
|
||||
description="A featureful, immutable, and correct URL for Python.",
|
||||
long_description=__doc__,
|
||||
author=__author__,
|
||||
author_email=__contact__,
|
||||
url=__url__,
|
||||
packages=find_packages(where="src"),
|
||||
package_dir={"": "src"},
|
||||
package_data=dict(
|
||||
hyperlink=[
|
||||
"py.typed",
|
||||
],
|
||||
),
|
||||
zip_safe=False,
|
||||
license=__license__,
|
||||
platforms='any',
|
||||
install_requires=[
|
||||
'idna>=2.5',
|
||||
'typing ; python_version<"3.5"',
|
||||
],
|
||||
python_requires='>=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
|
||||
classifiers=[
|
||||
'Topic :: Utilities',
|
||||
'Intended Audience :: Developers',
|
||||
'Topic :: Software Development :: Libraries',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
'License :: OSI Approved :: MIT License', ]
|
||||
)
|
||||
setup(
|
||||
name="hyperlink",
|
||||
version=__version__,
|
||||
description="A featureful, immutable, and correct URL for Python.",
|
||||
long_description=__doc__,
|
||||
author=__author__,
|
||||
author_email=__contact__,
|
||||
url=__url__,
|
||||
packages=find_packages(where="src"),
|
||||
package_dir={"": "src"},
|
||||
package_data=dict(hyperlink=["py.typed",],),
|
||||
zip_safe=False,
|
||||
license=__license__,
|
||||
platforms="any",
|
||||
install_requires=["idna>=2.5", 'typing ; python_version<"3.5"',],
|
||||
python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
|
||||
classifiers=[
|
||||
"Topic :: Utilities",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.6",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
],
|
||||
)
|
||||
|
||||
"""
|
||||
A brief checklist for release:
|
||||
|
|
|
@ -2,6 +2,7 @@ try:
|
|||
from socket import inet_pton
|
||||
except ImportError:
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
pass
|
||||
else:
|
||||
|
@ -25,7 +26,7 @@ except ImportError:
|
|||
def inet_pton(address_family, ip_string):
|
||||
# type: (int, str) -> bytes
|
||||
addr = SockAddr()
|
||||
ip_string_bytes = ip_string.encode('ascii')
|
||||
ip_string_bytes = ip_string.encode("ascii")
|
||||
addr.sa_family = address_family
|
||||
addr_size = ctypes.c_int(ctypes.sizeof(addr))
|
||||
|
||||
|
@ -37,10 +38,16 @@ except ImportError:
|
|||
except KeyError:
|
||||
raise socket.error("unknown address family")
|
||||
|
||||
if WSAStringToAddressA(
|
||||
ip_string_bytes, address_family, None,
|
||||
ctypes.byref(addr), ctypes.byref(addr_size)
|
||||
) != 0:
|
||||
if (
|
||||
WSAStringToAddressA(
|
||||
ip_string_bytes,
|
||||
address_family,
|
||||
None,
|
||||
ctypes.byref(addr),
|
||||
ctypes.byref(addr_size),
|
||||
)
|
||||
!= 0
|
||||
):
|
||||
raise socket.error(ctypes.FormatError())
|
||||
|
||||
return ctypes.string_at(getattr(addr, attribute), size)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,12 +6,13 @@ class HyperlinkTestCase(TestCase):
|
|||
"""This type mostly exists to provide a backwards-compatible
|
||||
assertRaises method for Python 2.6 testing.
|
||||
"""
|
||||
|
||||
def assertRaises( # type: ignore[override]
|
||||
self,
|
||||
expected_exception, # type: Type[BaseException]
|
||||
callableObj=None, # type: Optional[Callable[..., Any]]
|
||||
*args, # type: Any
|
||||
**kwargs # type: Any
|
||||
callableObj=None, # type: Optional[Callable[..., Any]]
|
||||
*args, # type: Any
|
||||
**kwargs # type: Any
|
||||
):
|
||||
# type: (...) -> Any
|
||||
"""Fail unless an exception of class expected_exception is raised
|
||||
|
|
|
@ -39,8 +39,9 @@ class TestHyperlink(TestCase):
|
|||
called_with.append((args, kwargs))
|
||||
raise _ExpectedException
|
||||
|
||||
self.hyperlink_test.assertRaises(_ExpectedException,
|
||||
raisesExpected, 1, keyword=True)
|
||||
self.hyperlink_test.assertRaises(
|
||||
_ExpectedException, raisesExpected, 1, keyword=True
|
||||
)
|
||||
self.assertEqual(called_with, [((1,), {"keyword": True})])
|
||||
|
||||
def test_assertRaisesWithCallableUnexpectedException(self):
|
||||
|
@ -55,8 +56,9 @@ class TestHyperlink(TestCase):
|
|||
raise _UnexpectedException
|
||||
|
||||
try:
|
||||
self.hyperlink_test.assertRaises(_ExpectedException,
|
||||
doesNotRaiseExpected)
|
||||
self.hyperlink_test.assertRaises(
|
||||
_ExpectedException, doesNotRaiseExpected
|
||||
)
|
||||
except _UnexpectedException:
|
||||
pass
|
||||
|
||||
|
@ -72,8 +74,7 @@ class TestHyperlink(TestCase):
|
|||
pass
|
||||
|
||||
try:
|
||||
self.hyperlink_test.assertRaises(_ExpectedException,
|
||||
doesNotRaise)
|
||||
self.hyperlink_test.assertRaises(_ExpectedException, doesNotRaise)
|
||||
except AssertionError:
|
||||
pass
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from .. import DecodedURL, URL
|
|||
from .._url import _percent_decode
|
||||
from .common import HyperlinkTestCase
|
||||
|
||||
BASIC_URL = 'http://example.com/#'
|
||||
BASIC_URL = "http://example.com/#"
|
||||
TOTAL_URL = (
|
||||
"https://%75%73%65%72:%00%00%00%00@xn--bcher-kva.ch:8080/"
|
||||
"a/nice%20nice/./path/?zot=23%25&zut#frég"
|
||||
|
@ -15,27 +15,26 @@ TOTAL_URL = (
|
|||
|
||||
|
||||
class TestURL(HyperlinkTestCase):
|
||||
|
||||
def test_durl_basic(self):
|
||||
# type: () -> None
|
||||
bdurl = DecodedURL.from_text(BASIC_URL)
|
||||
assert bdurl.scheme == 'http'
|
||||
assert bdurl.host == 'example.com'
|
||||
assert bdurl.scheme == "http"
|
||||
assert bdurl.host == "example.com"
|
||||
assert bdurl.port == 80
|
||||
assert bdurl.path == ('',)
|
||||
assert bdurl.fragment == ''
|
||||
assert bdurl.path == ("",)
|
||||
assert bdurl.fragment == ""
|
||||
|
||||
durl = DecodedURL.from_text(TOTAL_URL)
|
||||
|
||||
assert durl.scheme == 'https'
|
||||
assert durl.host == 'bücher.ch'
|
||||
assert durl.scheme == "https"
|
||||
assert durl.host == "bücher.ch"
|
||||
assert durl.port == 8080
|
||||
assert durl.path == ('a', 'nice nice', '.', 'path', '')
|
||||
assert durl.fragment == 'frég'
|
||||
assert durl.get('zot') == ['23%']
|
||||
assert durl.path == ("a", "nice nice", ".", "path", "")
|
||||
assert durl.fragment == "frég"
|
||||
assert durl.get("zot") == ["23%"]
|
||||
|
||||
assert durl.user == 'user'
|
||||
assert durl.userinfo == ('user', '\0\0\0\0')
|
||||
assert durl.user == "user"
|
||||
assert durl.userinfo == ("user", "\0\0\0\0")
|
||||
|
||||
def test_passthroughs(self):
|
||||
# type: () -> None
|
||||
|
@ -44,18 +43,18 @@ class TestURL(HyperlinkTestCase):
|
|||
# through to the underlying URL
|
||||
|
||||
durl = DecodedURL.from_text(TOTAL_URL)
|
||||
assert durl.sibling('te%t').path[-1] == 'te%t'
|
||||
assert durl.child('../test2%').path[-1] == '../test2%'
|
||||
assert durl.sibling("te%t").path[-1] == "te%t"
|
||||
assert durl.child("../test2%").path[-1] == "../test2%"
|
||||
assert durl.child() == durl
|
||||
assert durl.child() is durl
|
||||
assert durl.click('/').path[-1] == ''
|
||||
assert durl.user == 'user'
|
||||
assert durl.click("/").path[-1] == ""
|
||||
assert durl.user == "user"
|
||||
|
||||
assert '.' in durl.path
|
||||
assert '.' not in durl.normalize().path
|
||||
assert "." in durl.path
|
||||
assert "." not in durl.normalize().path
|
||||
|
||||
assert durl.to_uri().fragment == 'fr%C3%A9g'
|
||||
assert ' ' in durl.to_iri().path[1]
|
||||
assert durl.to_uri().fragment == "fr%C3%A9g"
|
||||
assert " " in durl.to_iri().path[1]
|
||||
|
||||
assert durl.to_text(with_password=True) == TOTAL_URL
|
||||
|
||||
|
@ -68,8 +67,8 @@ class TestURL(HyperlinkTestCase):
|
|||
assert durl2 == durl2.encoded_url.get_decoded_url(lazy=True)
|
||||
|
||||
assert (
|
||||
str(DecodedURL.from_text(BASIC_URL).child(' ')) ==
|
||||
'http://example.com/%20'
|
||||
str(DecodedURL.from_text(BASIC_URL).child(" "))
|
||||
== "http://example.com/%20"
|
||||
)
|
||||
|
||||
assert not (durl == 1)
|
||||
|
@ -78,46 +77,42 @@ class TestURL(HyperlinkTestCase):
|
|||
def test_repr(self):
|
||||
# type: () -> None
|
||||
durl = DecodedURL.from_text(TOTAL_URL)
|
||||
assert repr(durl) == 'DecodedURL(url=' + repr(durl._url) + ')'
|
||||
assert repr(durl) == "DecodedURL(url=" + repr(durl._url) + ")"
|
||||
|
||||
def test_query_manipulation(self):
|
||||
# type: () -> None
|
||||
durl = DecodedURL.from_text(TOTAL_URL)
|
||||
|
||||
assert durl.get('zot') == ['23%']
|
||||
durl = durl.add(' ', 'space')
|
||||
assert durl.get(' ') == ['space']
|
||||
durl = durl.set(' ', 'spa%ed')
|
||||
assert durl.get(' ') == ['spa%ed']
|
||||
assert durl.get("zot") == ["23%"]
|
||||
durl = durl.add(" ", "space")
|
||||
assert durl.get(" ") == ["space"]
|
||||
durl = durl.set(" ", "spa%ed")
|
||||
assert durl.get(" ") == ["spa%ed"]
|
||||
|
||||
durl = DecodedURL(url=durl.to_uri())
|
||||
assert durl.get(' ') == ['spa%ed']
|
||||
durl = durl.remove(' ')
|
||||
assert durl.get(' ') == []
|
||||
assert durl.get(" ") == ["spa%ed"]
|
||||
durl = durl.remove(" ")
|
||||
assert durl.get(" ") == []
|
||||
|
||||
durl = DecodedURL.from_text('/?%61rg=b&arg=c')
|
||||
assert durl.get('arg') == ['b', 'c']
|
||||
durl = DecodedURL.from_text("/?%61rg=b&arg=c")
|
||||
assert durl.get("arg") == ["b", "c"]
|
||||
|
||||
assert durl.set('arg', 'd').get('arg') == ['d']
|
||||
assert durl.set("arg", "d").get("arg") == ["d"]
|
||||
|
||||
durl = DecodedURL.from_text(
|
||||
u"https://example.com/a/b/?fóó=1&bar=2&fóó=3"
|
||||
"https://example.com/a/b/?fóó=1&bar=2&fóó=3"
|
||||
)
|
||||
assert (
|
||||
durl.remove("fóó") ==
|
||||
DecodedURL.from_text("https://example.com/a/b/?bar=2")
|
||||
assert durl.remove("fóó") == DecodedURL.from_text(
|
||||
"https://example.com/a/b/?bar=2"
|
||||
)
|
||||
assert (
|
||||
durl.remove("fóó", value="1") ==
|
||||
DecodedURL.from_text("https://example.com/a/b/?bar=2&fóó=3")
|
||||
assert durl.remove("fóó", value="1") == DecodedURL.from_text(
|
||||
"https://example.com/a/b/?bar=2&fóó=3"
|
||||
)
|
||||
assert (
|
||||
durl.remove("fóó", limit=1) ==
|
||||
DecodedURL.from_text("https://example.com/a/b/?bar=2&fóó=3")
|
||||
assert durl.remove("fóó", limit=1) == DecodedURL.from_text(
|
||||
"https://example.com/a/b/?bar=2&fóó=3"
|
||||
)
|
||||
assert (
|
||||
durl.remove("fóó", value="1", limit=0) ==
|
||||
DecodedURL.from_text("https://example.com/a/b/?fóó=1&bar=2&fóó=3")
|
||||
assert durl.remove("fóó", value="1", limit=0) == DecodedURL.from_text(
|
||||
"https://example.com/a/b/?fóó=1&bar=2&fóó=3"
|
||||
)
|
||||
|
||||
def test_equality_and_hashability(self):
|
||||
|
@ -153,15 +148,17 @@ class TestURL(HyperlinkTestCase):
|
|||
# type: () -> None
|
||||
durl = DecodedURL.from_text(TOTAL_URL)
|
||||
|
||||
durl2 = durl.replace(scheme=durl.scheme,
|
||||
host=durl.host,
|
||||
path=durl.path,
|
||||
query=durl.query,
|
||||
fragment=durl.fragment,
|
||||
port=durl.port,
|
||||
rooted=durl.rooted,
|
||||
userinfo=durl.userinfo,
|
||||
uses_netloc=durl.uses_netloc)
|
||||
durl2 = durl.replace(
|
||||
scheme=durl.scheme,
|
||||
host=durl.host,
|
||||
path=durl.path,
|
||||
query=durl.query,
|
||||
fragment=durl.fragment,
|
||||
port=durl.port,
|
||||
rooted=durl.rooted,
|
||||
userinfo=durl.userinfo,
|
||||
uses_netloc=durl.uses_netloc,
|
||||
)
|
||||
|
||||
assert durl == durl2
|
||||
|
||||
|
@ -171,7 +168,9 @@ class TestURL(HyperlinkTestCase):
|
|||
with self.assertRaises(ValueError):
|
||||
durl.replace(
|
||||
userinfo=( # type: ignore[arg-type]
|
||||
'user', 'pw', 'thiswillcauseafailure'
|
||||
"user",
|
||||
"pw",
|
||||
"thiswillcauseafailure",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
@ -181,8 +180,8 @@ class TestURL(HyperlinkTestCase):
|
|||
durl = DecodedURL.from_text(TOTAL_URL)
|
||||
|
||||
assert durl == DecodedURL.fromText(TOTAL_URL)
|
||||
assert 'to_text' in dir(durl)
|
||||
assert 'asText' not in dir(durl)
|
||||
assert "to_text" in dir(durl)
|
||||
assert "asText" not in dir(durl)
|
||||
assert durl.to_text() == durl.asText()
|
||||
|
||||
def test_percent_decode_mixed(self):
|
||||
|
@ -190,24 +189,24 @@ class TestURL(HyperlinkTestCase):
|
|||
|
||||
# See https://github.com/python-hyper/hyperlink/pull/59 for a
|
||||
# nice discussion of the possibilities
|
||||
assert _percent_decode('abcdé%C3%A9éfg') == 'abcdéééfg'
|
||||
assert _percent_decode("abcdé%C3%A9éfg") == "abcdéééfg"
|
||||
|
||||
# still allow percent encoding in the case of an error
|
||||
assert _percent_decode('abcdé%C3éfg') == 'abcdé%C3éfg'
|
||||
assert _percent_decode("abcdé%C3éfg") == "abcdé%C3éfg"
|
||||
|
||||
# ...unless explicitly told otherwise
|
||||
with self.assertRaises(UnicodeDecodeError):
|
||||
_percent_decode('abcdé%C3éfg', raise_subencoding_exc=True)
|
||||
_percent_decode("abcdé%C3éfg", raise_subencoding_exc=True)
|
||||
|
||||
# when not encodable as subencoding
|
||||
assert _percent_decode('é%25é', subencoding='ascii') == 'é%25é'
|
||||
assert _percent_decode("é%25é", subencoding="ascii") == "é%25é"
|
||||
|
||||
def test_click_decoded_url(self):
|
||||
# type: () -> None
|
||||
durl = DecodedURL.from_text(TOTAL_URL)
|
||||
durl_dest = DecodedURL.from_text('/tëst')
|
||||
durl_dest = DecodedURL.from_text("/tëst")
|
||||
|
||||
clicked = durl.click(durl_dest)
|
||||
assert clicked.host == durl.host
|
||||
assert clicked.path == durl_dest.path
|
||||
assert clicked.path == ('tëst',)
|
||||
assert clicked.path == ("tëst",)
|
||||
|
|
|
@ -5,29 +5,28 @@ from __future__ import unicode_literals
|
|||
from .common import HyperlinkTestCase
|
||||
from hyperlink import parse, EncodedURL, DecodedURL
|
||||
|
||||
BASIC_URL = 'http://example.com/#'
|
||||
BASIC_URL = "http://example.com/#"
|
||||
TOTAL_URL = (
|
||||
"https://%75%73%65%72:%00%00%00%00@xn--bcher-kva.ch:8080"
|
||||
"/a/nice%20nice/./path/?zot=23%25&zut#frég"
|
||||
)
|
||||
UNDECODABLE_FRAG_URL = TOTAL_URL + '%C3'
|
||||
UNDECODABLE_FRAG_URL = TOTAL_URL + "%C3"
|
||||
# the %C3 above percent-decodes to an unpaired \xc3 byte which makes this
|
||||
# invalid utf8
|
||||
|
||||
|
||||
class TestURL(HyperlinkTestCase):
|
||||
|
||||
def test_parse(self):
|
||||
# type: () -> None
|
||||
purl = parse(TOTAL_URL)
|
||||
assert isinstance(purl, DecodedURL)
|
||||
assert purl.user == 'user'
|
||||
assert purl.get('zot') == ['23%']
|
||||
assert purl.fragment == 'frég'
|
||||
assert purl.user == "user"
|
||||
assert purl.get("zot") == ["23%"]
|
||||
assert purl.fragment == "frég"
|
||||
|
||||
purl2 = parse(TOTAL_URL, decoded=False)
|
||||
assert isinstance(purl2, EncodedURL)
|
||||
assert purl2.get('zot') == ['23%25']
|
||||
assert purl2.get("zot") == ["23%25"]
|
||||
|
||||
with self.assertRaises(UnicodeDecodeError):
|
||||
purl3 = parse(UNDECODABLE_FRAG_URL)
|
||||
|
|
|
@ -9,7 +9,6 @@ from .._url import register_scheme, URL
|
|||
|
||||
|
||||
class TestSchemeRegistration(HyperlinkTestCase):
|
||||
|
||||
def setUp(self):
|
||||
# type: () -> None
|
||||
self._orig_scheme_port_map = dict(_url.SCHEME_PORT_MAP)
|
||||
|
@ -22,52 +21,52 @@ class TestSchemeRegistration(HyperlinkTestCase):
|
|||
|
||||
def test_register_scheme_basic(self):
|
||||
# type: () -> None
|
||||
register_scheme('deltron', uses_netloc=True, default_port=3030)
|
||||
register_scheme("deltron", uses_netloc=True, default_port=3030)
|
||||
|
||||
u1 = URL.from_text('deltron://example.com')
|
||||
assert u1.scheme == 'deltron'
|
||||
u1 = URL.from_text("deltron://example.com")
|
||||
assert u1.scheme == "deltron"
|
||||
assert u1.port == 3030
|
||||
assert u1.uses_netloc is True
|
||||
|
||||
# test netloc works even when the original gives no indication
|
||||
u2 = URL.from_text('deltron:')
|
||||
u2 = u2.replace(host='example.com')
|
||||
assert u2.to_text() == 'deltron://example.com'
|
||||
u2 = URL.from_text("deltron:")
|
||||
u2 = u2.replace(host="example.com")
|
||||
assert u2.to_text() == "deltron://example.com"
|
||||
|
||||
# test default port means no emission
|
||||
u3 = URL.from_text('deltron://example.com:3030')
|
||||
assert u3.to_text() == 'deltron://example.com'
|
||||
u3 = URL.from_text("deltron://example.com:3030")
|
||||
assert u3.to_text() == "deltron://example.com"
|
||||
|
||||
register_scheme('nonetron', default_port=3031)
|
||||
u4 = URL(scheme='nonetron')
|
||||
u4 = u4.replace(host='example.com')
|
||||
assert u4.to_text() == 'nonetron://example.com'
|
||||
register_scheme("nonetron", default_port=3031)
|
||||
u4 = URL(scheme="nonetron")
|
||||
u4 = u4.replace(host="example.com")
|
||||
assert u4.to_text() == "nonetron://example.com"
|
||||
|
||||
def test_register_no_netloc_scheme(self):
|
||||
# type: () -> None
|
||||
register_scheme('noloctron', uses_netloc=False)
|
||||
u4 = URL(scheme='noloctron')
|
||||
register_scheme("noloctron", uses_netloc=False)
|
||||
u4 = URL(scheme="noloctron")
|
||||
u4 = u4.replace(path=("example", "path"))
|
||||
assert u4.to_text() == 'noloctron:example/path'
|
||||
assert u4.to_text() == "noloctron:example/path"
|
||||
|
||||
def test_register_no_netloc_with_port(self):
|
||||
# type: () -> None
|
||||
with self.assertRaises(ValueError):
|
||||
register_scheme('badnetlocless', uses_netloc=False, default_port=7)
|
||||
register_scheme("badnetlocless", uses_netloc=False, default_port=7)
|
||||
|
||||
def test_invalid_uses_netloc(self):
|
||||
# type: () -> None
|
||||
with self.assertRaises(ValueError):
|
||||
register_scheme('badnetloc', uses_netloc=cast(bool, None))
|
||||
register_scheme("badnetloc", uses_netloc=cast(bool, None))
|
||||
with self.assertRaises(ValueError):
|
||||
register_scheme('badnetloc', uses_netloc=cast(bool, object()))
|
||||
register_scheme("badnetloc", uses_netloc=cast(bool, object()))
|
||||
|
||||
def test_register_invalid_uses_netloc(self):
|
||||
# type: () -> None
|
||||
with self.assertRaises(ValueError):
|
||||
register_scheme('lol', uses_netloc=cast(bool, object()))
|
||||
register_scheme("lol", uses_netloc=cast(bool, object()))
|
||||
|
||||
def test_register_invalid_port(self):
|
||||
# type: () -> None
|
||||
with self.assertRaises(ValueError):
|
||||
register_scheme('nope', default_port=cast(bool, object()))
|
||||
register_scheme("nope", default_port=cast(bool, object()))
|
||||
|
|
File diff suppressed because it is too large
Load Diff
172
tox.ini
172
tox.ini
|
@ -1,17 +1,30 @@
|
|||
[tox]
|
||||
|
||||
envlist =
|
||||
flake8, mypy
|
||||
test-py{26,27,34,35,36,37,38,py,py3}
|
||||
flake8, mypy, black
|
||||
test-py{26,27,34,35,36,37,38,py2,py3}
|
||||
coverage_report
|
||||
packaging
|
||||
docs
|
||||
packaging
|
||||
|
||||
skip_missing_interpreters = {tty:True:False}
|
||||
|
||||
|
||||
[default]
|
||||
|
||||
basepython = python3.8
|
||||
|
||||
deps =
|
||||
idna==2.9
|
||||
|
||||
setenv =
|
||||
PY_MODULE=hyperlink
|
||||
|
||||
PYTHONPYCACHEPREFIX={envtmpdir}/pycache
|
||||
|
||||
|
||||
##
|
||||
# Build (default environment)
|
||||
# Default environment: unit tests
|
||||
##
|
||||
|
||||
[testenv]
|
||||
|
@ -31,52 +44,62 @@ basepython =
|
|||
pypy3: pypy3
|
||||
|
||||
deps =
|
||||
test: coverage==4.5.4 # rq.filter: <5
|
||||
test-py27: hypothesis==4.43.3
|
||||
{[default]deps}
|
||||
|
||||
{py26,py27}: typing==3.7.4.1
|
||||
|
||||
{py26,py27,py34}: pytest==4.6.9
|
||||
{py35,py36,py37,py38}: pytest==5.2.4
|
||||
py27: mock==3.0.5
|
||||
|
||||
{[testenv:coverage_report]deps}
|
||||
pytest-cov==2.8.1
|
||||
|
||||
# py34 isn't supported by hypothesis
|
||||
test-py35: hypothesis==4.43.3
|
||||
test-py36: hypothesis==4.43.3
|
||||
test-py37: hypothesis==4.43.3
|
||||
test-py38: hypothesis==4.43.3
|
||||
test-py39: hypothesis==4.43.3
|
||||
test: idna==2.9
|
||||
test-py27: mock==3.0.5
|
||||
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
|
||||
|
||||
passenv =
|
||||
# For Hypothesis settings
|
||||
test: CI
|
||||
|
||||
# See https://github.com/codecov/codecov-python/blob/master/README.md#using-tox
|
||||
# And CI-specific docs:
|
||||
# https://help.github.com/en/articles/virtual-environments-for-github-actions#default-environment-variables
|
||||
# https://docs.travis-ci.com/user/environment-variables#default-environment-variables
|
||||
# https://www.appveyor.com/docs/environment-variables/
|
||||
codecov: TOXENV CODECOV_* CI
|
||||
codecov: GITHUB_*
|
||||
codecov: TRAVIS TRAVIS_*
|
||||
codecov: APPVEYOR APPVEYOR_*
|
||||
|
||||
# Used in our AppVeyor config
|
||||
codecov: OS
|
||||
py27: hypothesis==4.43.3 # rq.filter: <4.44
|
||||
{py35,py36,py37,py38,py39,py2,py3}: hypothesis==5.8.4
|
||||
|
||||
setenv =
|
||||
PY_MODULE=hyperlink
|
||||
|
||||
test: HYPOTHESIS_STORAGE_DIRECTORY={toxworkdir}/hypothesis
|
||||
test: PYTHONPYCACHEPREFIX={envtmpdir}/pycache
|
||||
{[default]setenv}
|
||||
|
||||
test: COVERAGE_FILE={toxworkdir}/coverage.{envname}
|
||||
{coverage_report,codecov}: COVERAGE_FILE={toxworkdir}/coverage
|
||||
codecov: COVERAGE_XML={envlogdir}/coverage_report.xml
|
||||
|
||||
commands =
|
||||
test: pytest --cov={env:PY_MODULE} --cov-report=term-missing:skip-covered --doctest-modules {posargs:src/{env:PY_MODULE}}
|
||||
|
||||
|
||||
##
|
||||
# Black code formatting
|
||||
##
|
||||
|
||||
[testenv:black]
|
||||
|
||||
description = run Black (linter)
|
||||
|
||||
basepython = {[default]basepython}
|
||||
|
||||
skip_install = True
|
||||
|
||||
deps =
|
||||
black==19.10b0
|
||||
|
||||
setenv =
|
||||
BLACK_LINT_ARGS=--check
|
||||
|
||||
commands =
|
||||
black {env:BLACK_LINT_ARGS:} setup.py src
|
||||
|
||||
|
||||
[testenv:black-reformat]
|
||||
|
||||
description = {[testenv:black]description} and reformat
|
||||
basepython = {[testenv:black]basepython}
|
||||
skip_install = {[testenv:black]skip_install}
|
||||
deps = {[testenv:black]deps}
|
||||
commands = {[testenv:black]commands}
|
||||
|
||||
|
||||
##
|
||||
# Flake8 linting
|
||||
##
|
||||
|
@ -85,16 +108,16 @@ commands =
|
|||
|
||||
description = run Flake8 (linter)
|
||||
|
||||
basepython = python3.8
|
||||
basepython = {[default]basepython}
|
||||
|
||||
skip_install = True
|
||||
|
||||
deps =
|
||||
flake8-bugbear==20.1.4
|
||||
#flake8-docstrings==1.5.0
|
||||
flake8==3.7.9
|
||||
mccabe==0.6.1
|
||||
pep8-naming==0.10.0
|
||||
pycodestyle==2.5.0
|
||||
pydocstyle==5.0.2
|
||||
# pin pyflakes pending a release with https://github.com/PyCQA/pyflakes/pull/455
|
||||
git+git://github.com/PyCQA/pyflakes@ffe9386#egg=pyflakes
|
||||
|
@ -111,6 +134,8 @@ select = A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z
|
|||
show-source = True
|
||||
doctests = True
|
||||
|
||||
max-line-length = 80
|
||||
|
||||
# Codes: http://flake8.pycqa.org/en/latest/user/error-codes.html
|
||||
ignore =
|
||||
# syntax error in type comment
|
||||
|
@ -131,8 +156,8 @@ ignore =
|
|||
# variable in global scope should not be mixedCase
|
||||
N816,
|
||||
|
||||
# line break after binary operator
|
||||
W504,
|
||||
# line break before binary operator
|
||||
W503,
|
||||
|
||||
# End of list (allows last item to end with trailing ',')
|
||||
EOL
|
||||
|
@ -149,13 +174,13 @@ application-import-names = deploy
|
|||
|
||||
description = run Mypy (static type checker)
|
||||
|
||||
basepython = python3.8
|
||||
|
||||
skip_install = True
|
||||
basepython = {[default]basepython}
|
||||
|
||||
deps =
|
||||
mypy==0.770
|
||||
|
||||
{[default]deps}
|
||||
|
||||
commands =
|
||||
mypy \
|
||||
--config-file="{toxinidir}/tox.ini" \
|
||||
|
@ -182,11 +207,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
|
||||
|
@ -210,12 +231,19 @@ ignore_missing_imports = True
|
|||
|
||||
description = generate coverage report
|
||||
|
||||
basepython = python
|
||||
depends = test-py{26,27,34,35,36,37,38,py,py3}
|
||||
|
||||
basepython = {[default]basepython}
|
||||
|
||||
skip_install = True
|
||||
|
||||
deps =
|
||||
coverage==4.5.4
|
||||
coverage==4.5.4 # rq.filter: <5 # coverage 5.0 drops Python 3.4 support
|
||||
|
||||
setenv =
|
||||
{[default]setenv}
|
||||
|
||||
COVERAGE_FILE={toxworkdir}/coverage
|
||||
|
||||
commands =
|
||||
coverage combine
|
||||
|
@ -231,17 +259,34 @@ commands =
|
|||
|
||||
description = upload coverage to Codecov
|
||||
|
||||
depends = {[coverage_report]depends}
|
||||
|
||||
basepython = python
|
||||
|
||||
skip_install = True
|
||||
|
||||
deps =
|
||||
coverage==4.5.4
|
||||
{[testenv:coverage_report]deps}
|
||||
codecov==2.0.22
|
||||
|
||||
commands =
|
||||
# Note documentation for CI variables in default environment's passenv
|
||||
passenv =
|
||||
# See https://github.com/codecov/codecov-python/blob/master/README.md#using-tox
|
||||
# And CI-specific docs:
|
||||
# https://help.github.com/en/articles/virtual-environments-for-github-actions#default-environment-variables
|
||||
# https://docs.travis-ci.com/user/environment-variables#default-environment-variables
|
||||
# https://www.appveyor.com/docs/environment-variables/
|
||||
TOXENV CODECOV_* CI
|
||||
GITHUB_*
|
||||
TRAVIS TRAVIS_*
|
||||
APPVEYOR APPVEYOR_*
|
||||
|
||||
setenv =
|
||||
{[testenv:coverage_report]setenv}
|
||||
|
||||
COVERAGE_XML={envlogdir}/coverage_report.xml
|
||||
|
||||
commands =
|
||||
# Note documentation for CI variables in passenv above
|
||||
coverage combine
|
||||
coverage xml -o "{env:COVERAGE_XML}"
|
||||
codecov --file="{env:COVERAGE_XML}" --env \
|
||||
|
@ -261,28 +306,27 @@ commands =
|
|||
|
||||
description = build documentation
|
||||
|
||||
basepython = python3.8
|
||||
basepython = {[default]basepython}
|
||||
|
||||
deps =
|
||||
Sphinx==2.3.1
|
||||
Sphinx==2.4.4
|
||||
sphinx-rtd-theme==0.4.3
|
||||
|
||||
commands =
|
||||
sphinx-build \
|
||||
-b html -d "{envtmpdir}/doctrees" \
|
||||
"{toxinidir}/docs" \
|
||||
"{toxworkdir}/docs/html"
|
||||
"{toxinidir}/htmldocs"
|
||||
|
||||
|
||||
[testenv:docs-auto]
|
||||
|
||||
description = build documentation and rebuild automatically
|
||||
|
||||
basepython = python3.8
|
||||
basepython = {[default]basepython}
|
||||
|
||||
deps =
|
||||
Sphinx==2.2.2
|
||||
sphinx-rtd-theme==0.4.3
|
||||
{[testenv:docs]deps}
|
||||
sphinx-autobuild==0.7.1
|
||||
|
||||
commands =
|
||||
|
@ -290,7 +334,7 @@ commands =
|
|||
-b html -d "{envtmpdir}/doctrees" \
|
||||
--host=localhost \
|
||||
"{toxinidir}/docs" \
|
||||
"{toxworkdir}/docs/html"
|
||||
"{toxinidir}/htmldocs"
|
||||
|
||||
|
||||
##
|
||||
|
@ -301,7 +345,9 @@ commands =
|
|||
|
||||
description = check for potential packaging problems
|
||||
|
||||
basepython = python
|
||||
basepython = {[default]basepython}
|
||||
|
||||
skip_install = True
|
||||
|
||||
deps =
|
||||
check-manifest==0.41
|
||||
|
|
Loading…
Reference in New Issue