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:
Wilfredo Sánchez 2020-04-14 17:18:58 -07:00
commit 1b1e12a62e
14 changed files with 1517 additions and 1046 deletions

23
.gitignore vendored
View File

@ -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

View File

@ -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"

View File

@ -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

10
pyproject.toml Normal file
View File

@ -0,0 +1,10 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 80
target-version = ["py27"]

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",)

View File

@ -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)

View File

@ -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
View File

@ -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