Skip to content

Commit a07bc94

Browse files
authored
Add support for multiversion testing with tox (#21)
Also full coverage for all supported Pythons.
1 parent 71651e9 commit a07bc94

File tree

10 files changed

+144
-34
lines changed

10 files changed

+144
-34
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,4 @@ ENV/
9999

100100
# mypy
101101
.mypy_cache/
102+
/diffcov.html

coverage.ini

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[run]
2+
branch = true
3+
parallel = true
4+
omit =
5+
setup*
6+
.tox/*/lib/python3.*/site-packages/*
7+
.tox/*/lib/python3.*/site-packages/*
8+
*/tests/*.py
9+
/tmp/*
10+
/private/var/folders/*
11+
*/testing/*.py
12+
13+
[report]
14+
exclude_lines =
15+
pragma: nocover
16+
pragma: missed
17+
pragma: ge${GEVER}
18+
pragma: le${LEVER}
19+
raise NotImplementedError
20+
raise AssertionError
21+
assert\s
22+
23+
[paths]
24+
source =
25+
importlib_resources
26+
.tox/*/lib/python*/site-packages/importlib_resources

importlib_resources/__init__.py

+11-10
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@
2020

2121
Package = Union[types.ModuleType, str]
2222
if sys.version_info >= (3, 6):
23-
FileName = Union[str, os.PathLike]
23+
FileName = Union[str, os.PathLike] # pragma: ge36
2424
else:
25-
FileName = str
25+
FileName = str # pragma: le35
2626

2727

2828
def _get_package(package) -> types.ModuleType:
2929
if hasattr(package, '__spec__'):
3030
if package.__spec__.submodule_search_locations is None:
31-
raise TypeError("{!r} is not a package".format(package.__spec__.name))
31+
raise TypeError("{!r} is not a package".format(
32+
package.__spec__.name))
3233
else:
3334
return package
3435
else:
@@ -69,13 +70,13 @@ def open(package: Package, file_name: FileName) -> BinaryIO:
6970
# importlib.machinery loaders are and an AttributeError for
7071
# get_data() will make it clear what is needed from the loader.
7172
loader = typing.cast(importlib.abc.ResourceLoader,
72-
package.__spec__.loader)
73+
package.__spec__.loader)
7374
try:
7475
data = loader.get_data(full_path)
7576
except IOError:
7677
package_name = package.__spec__.name
77-
message = '{!r} resource not found in {!r}'.format(file_name,
78-
package_name)
78+
message = '{!r} resource not found in {!r}'.format(
79+
file_name, package_name)
7980
raise FileNotFoundError(message)
8081
else:
8182
return io.BytesIO(data)
@@ -92,8 +93,8 @@ def read(package: Package, file_name: FileName, encoding: str = 'utf-8',
9293
package = _get_package(package)
9394
# Note this is **not** builtins.open()!
9495
with open(package, file_name) as binary_file:
95-
# Decoding from io.TextIOWrapper() instead of str.decode() in hopes that
96-
# the former will be smarter about memory usage.
96+
# Decoding from io.TextIOWrapper() instead of str.decode() in hopes
97+
# that the former will be smarter about memory usage.
9798
text_file = io.TextIOWrapper(binary_file, encoding=encoding,
9899
errors=errors)
99100
return text_file.read()
@@ -119,8 +120,8 @@ def path(package: Package, file_name: FileName) -> Iterator[pathlib.Path]:
119120
return
120121
except FileNotFoundError:
121122
pass
122-
# Fall-through for both the lack of resource_path() *and* if resource_path()
123-
# raises FileNotFoundError.
123+
# Fall-through for both the lack of resource_path() *and* if
124+
# resource_path() raises FileNotFoundError.
124125
package_directory = pathlib.Path(package.__spec__.origin).parent
125126
file_path = package_directory / file_name
126127
if file_path.exists():

importlib_resources/abc.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
11
import abc
2+
23
from typing.io import BinaryIO
34

45

56
class ResourceReader(abc.ABC):
6-
77
"""Abstract base class for loaders to provide resource reading support."""
88

99
@abc.abstractmethod
1010
def open_resource(self, path: str) -> BinaryIO:
11-
"""Return an opened, file-like object for binary reading of the resource.
11+
"""Return an opened, file-like object for binary reading.
1212
1313
The 'path' argument is expected to represent only a file name.
1414
If the resource cannot be found, FileNotFoundError is raised.
1515
"""
16-
raise FileNotFoundError
16+
# This deliberately raises FileNotFoundError instead of
17+
# NotImplementedError so that if this method is accidentally called,
18+
# it'll still do the right thing.
19+
raise FileNotFoundError # pragma: nocover
1720

21+
@abc.abstractmethod
1822
def resource_path(self, path: str) -> str:
1923
"""Return the file system path to the specified resource.
2024
2125
The 'path' argument is expected to represent only a file name.
2226
If the resource does not exist on the file system, raise
2327
FileNotFoundError.
2428
"""
25-
raise FileNotFoundError
29+
# This deliberately raises FileNotFoundError instead of
30+
# NotImplementedError so that if this method is accidentally called,
31+
# it'll still do the right thing.
32+
raise FileNotFoundError # pragma: nocover

importlib_resources/tests/test_open.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import io
2-
import os.path
3-
import pathlib
4-
import sys
52
import unittest
63

74
import importlib_resources as resources
@@ -33,7 +30,7 @@ def test_wrap_for_text(self):
3330

3431
def test_FileNotFoundError(self):
3532
with self.assertRaises(FileNotFoundError):
36-
with resources.open(self.data, 'does-not-exist') as file:
33+
with resources.open(self.data, 'does-not-exist'):
3734
pass
3835

3936

importlib_resources/tests/test_path.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
import io
2-
import os.path
3-
import pathlib
4-
import sys
51
import unittest
62

73
import importlib_resources as resources
@@ -35,7 +31,11 @@ class PathDiskTests(PathTests, unittest.TestCase):
3531

3632

3733
class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase):
38-
pass
34+
def test_remove_in_context_manager(self):
35+
# It is not an error if the file that was temporarily stashed on the
36+
# file system is removed inside the `with` stanza.
37+
with resources.path(self.data, 'utf-8.file') as path:
38+
path.unlink()
3939

4040

4141
if __name__ == '__main__':

importlib_resources/tests/test_read.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
import io
2-
import os.path
3-
import pathlib
4-
import sys
51
import unittest
62

73
import importlib_resources as resources
@@ -27,8 +23,8 @@ def test_encoding(self):
2723

2824
def test_errors(self):
2925
# Raises UnicodeError without the 'errors' argument.
30-
result = resources.read(self.data, 'utf-16.file', encoding='utf-8',
31-
errors='ignore')
26+
resources.read(
27+
self.data, 'utf-16.file', encoding='utf-8', errors='ignore')
3228

3329

3430
class ReadDiskTests(ReadTests, unittest.TestCase):

importlib_resources/tests/util.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from . import data
1212

1313

14-
def create_package(*, file, path):
14+
def create_package(*, file, path, is_package=True):
1515
class Reader(resources_abc.ResourceReader):
1616
def open_resource(self, path):
1717
self._path = path
@@ -28,9 +28,10 @@ def resource_path(self, path_):
2828
return path
2929

3030
name = 'testingpackage'
31-
spec = importlib.machinery.ModuleSpec(name, Reader(),
32-
origin='does-not-exist',
33-
is_package=True)
31+
spec = importlib.machinery.ModuleSpec(
32+
name, Reader(),
33+
origin='does-not-exist',
34+
is_package=is_package)
3435
# Unforunately importlib.util.module_from_spec() was not introduced until
3536
# Python 3.5.
3637
module = types.ModuleType(name)
@@ -80,11 +81,17 @@ def test_importing_module_as_side_effect(self):
8081
del sys.modules[data.__name__]
8182
self.execute(data.__name__, 'utf-8.file')
8283

83-
def test_non_package(self):
84+
def test_non_package_by_name(self):
8485
# The anchor package cannot be a module.
8586
with self.assertRaises(TypeError):
8687
self.execute(__spec__.name, 'utf-8.file')
8788

89+
def test_non_package_by_package(self):
90+
# The anchor package cannot be a module.
91+
with self.assertRaises(TypeError):
92+
module = sys.modules['importlib_resources.tests.util']
93+
self.execute(module, 'utf-8.file')
94+
8895
def test_resource_opener(self):
8996
data = io.BytesIO(b'Hello, world!')
9097
package = create_package(file=data, path=FileNotFoundError())

setup.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import sys
2+
3+
from setuptools import setup
4+
5+
6+
requirements = []
7+
if sys.version_info < (3,):
8+
requirements.append('pathlib2')
9+
10+
11+
setup(
12+
name='importlib_resources',
13+
install_requires=requirements,
14+
)

tox.ini

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
[tox]
2+
envlist = {py34,py35,py36,py37}-{nocov,cov,diffcov},qa
3+
skip_missing_interpreters = True
4+
5+
6+
[testenv]
7+
commands =
8+
nocov: python -m unittest discover
9+
cov,diffcov: python -m coverage run {[coverage]rc} -m unittest discover {posargs}
10+
cov,diffcov: python -m coverage combine {[coverage]rc}
11+
cov: python -m coverage html {[coverage]rc}
12+
cov: python -m coverage report -m {[coverage]rc} --fail-under=100
13+
diffcov: python -m coverage xml {[coverage]rc}
14+
diffcov: diff-cover coverage.xml --html-report diffcov.html
15+
diffcov: diff-cover coverage.xml --fail-under=100
16+
usedevelop = True
17+
passenv =
18+
PYTHON*
19+
LANG*
20+
LC_*
21+
deps =
22+
cov,diffcov: coverage
23+
diffcov: diff_cover
24+
setenv =
25+
cov: COVERAGE_PROCESS_START={[coverage]rcfile}
26+
cov: COVERAGE_OPTIONS="-p"
27+
cov: COVERAGE_FILE={toxinidir}/.coverage
28+
py34: GEVER=35
29+
py35: GEVER=36
30+
py36: GEVER=37
31+
py36: LEVER=35
32+
33+
34+
[testenv:qa]
35+
basepython = python3
36+
commands =
37+
python -m flake8 importlib_resources
38+
py34,py35,py36: mypy --ignore-missing-imports importlib_resources
39+
deps =
40+
py34,py35,py36: mypy
41+
flake8
42+
43+
44+
[testenv:docs]
45+
basepython = python3
46+
commands =
47+
python setup.py build_sphinx
48+
deps:
49+
sphinx
50+
docutils==0.12
51+
52+
53+
[coverage]
54+
rcfile = {toxinidir}/coverage.ini
55+
rc = --rcfile={[coverage]rcfile}
56+
57+
58+
[flake8]
59+
hang-closing = True
60+
jobs = 1
61+
max-line-length = 79

0 commit comments

Comments
 (0)