Skip to content

Commit

Permalink
Add support for multiversion testing with tox (python#21)
Browse files Browse the repository at this point in the history
Also full coverage for all supported Pythons.
  • Loading branch information
warsaw authored Nov 10, 2017
1 parent 71651e9 commit a07bc94
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ ENV/

# mypy
.mypy_cache/
/diffcov.html
26 changes: 26 additions & 0 deletions coverage.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[run]
branch = true
parallel = true
omit =
setup*
.tox/*/lib/python3.*/site-packages/*
.tox/*/lib/python3.*/site-packages/*
*/tests/*.py
/tmp/*
/private/var/folders/*
*/testing/*.py

[report]
exclude_lines =
pragma: nocover
pragma: missed
pragma: ge${GEVER}
pragma: le${LEVER}
raise NotImplementedError
raise AssertionError
assert\s

[paths]
source =
importlib_resources
.tox/*/lib/python*/site-packages/importlib_resources
21 changes: 11 additions & 10 deletions importlib_resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@

Package = Union[types.ModuleType, str]
if sys.version_info >= (3, 6):
FileName = Union[str, os.PathLike]
FileName = Union[str, os.PathLike] # pragma: ge36
else:
FileName = str
FileName = str # pragma: le35


def _get_package(package) -> types.ModuleType:
if hasattr(package, '__spec__'):
if package.__spec__.submodule_search_locations is None:
raise TypeError("{!r} is not a package".format(package.__spec__.name))
raise TypeError("{!r} is not a package".format(
package.__spec__.name))
else:
return package
else:
Expand Down Expand Up @@ -69,13 +70,13 @@ def open(package: Package, file_name: FileName) -> BinaryIO:
# importlib.machinery loaders are and an AttributeError for
# get_data() will make it clear what is needed from the loader.
loader = typing.cast(importlib.abc.ResourceLoader,
package.__spec__.loader)
package.__spec__.loader)
try:
data = loader.get_data(full_path)
except IOError:
package_name = package.__spec__.name
message = '{!r} resource not found in {!r}'.format(file_name,
package_name)
message = '{!r} resource not found in {!r}'.format(
file_name, package_name)
raise FileNotFoundError(message)
else:
return io.BytesIO(data)
Expand All @@ -92,8 +93,8 @@ def read(package: Package, file_name: FileName, encoding: str = 'utf-8',
package = _get_package(package)
# Note this is **not** builtins.open()!
with open(package, file_name) as binary_file:
# Decoding from io.TextIOWrapper() instead of str.decode() in hopes that
# the former will be smarter about memory usage.
# Decoding from io.TextIOWrapper() instead of str.decode() in hopes
# that the former will be smarter about memory usage.
text_file = io.TextIOWrapper(binary_file, encoding=encoding,
errors=errors)
return text_file.read()
Expand All @@ -119,8 +120,8 @@ def path(package: Package, file_name: FileName) -> Iterator[pathlib.Path]:
return
except FileNotFoundError:
pass
# Fall-through for both the lack of resource_path() *and* if resource_path()
# raises FileNotFoundError.
# Fall-through for both the lack of resource_path() *and* if
# resource_path() raises FileNotFoundError.
package_directory = pathlib.Path(package.__spec__.origin).parent
file_path = package_directory / file_name
if file_path.exists():
Expand Down
15 changes: 11 additions & 4 deletions importlib_resources/abc.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import abc

from typing.io import BinaryIO


class ResourceReader(abc.ABC):

"""Abstract base class for loaders to provide resource reading support."""

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

@abc.abstractmethod
def resource_path(self, path: str) -> str:
"""Return the file system path to the specified resource.
The 'path' argument is expected to represent only a file name.
If the resource does not exist on the file system, raise
FileNotFoundError.
"""
raise FileNotFoundError
# This deliberately raises FileNotFoundError instead of
# NotImplementedError so that if this method is accidentally called,
# it'll still do the right thing.
raise FileNotFoundError # pragma: nocover
5 changes: 1 addition & 4 deletions importlib_resources/tests/test_open.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import io
import os.path
import pathlib
import sys
import unittest

import importlib_resources as resources
Expand Down Expand Up @@ -33,7 +30,7 @@ def test_wrap_for_text(self):

def test_FileNotFoundError(self):
with self.assertRaises(FileNotFoundError):
with resources.open(self.data, 'does-not-exist') as file:
with resources.open(self.data, 'does-not-exist'):
pass


Expand Down
10 changes: 5 additions & 5 deletions importlib_resources/tests/test_path.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import io
import os.path
import pathlib
import sys
import unittest

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


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


if __name__ == '__main__':
Expand Down
8 changes: 2 additions & 6 deletions importlib_resources/tests/test_read.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import io
import os.path
import pathlib
import sys
import unittest

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

def test_errors(self):
# Raises UnicodeError without the 'errors' argument.
result = resources.read(self.data, 'utf-16.file', encoding='utf-8',
errors='ignore')
resources.read(
self.data, 'utf-16.file', encoding='utf-8', errors='ignore')


class ReadDiskTests(ReadTests, unittest.TestCase):
Expand Down
17 changes: 12 additions & 5 deletions importlib_resources/tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from . import data


def create_package(*, file, path):
def create_package(*, file, path, is_package=True):
class Reader(resources_abc.ResourceReader):
def open_resource(self, path):
self._path = path
Expand All @@ -28,9 +28,10 @@ def resource_path(self, path_):
return path

name = 'testingpackage'
spec = importlib.machinery.ModuleSpec(name, Reader(),
origin='does-not-exist',
is_package=True)
spec = importlib.machinery.ModuleSpec(
name, Reader(),
origin='does-not-exist',
is_package=is_package)
# Unforunately importlib.util.module_from_spec() was not introduced until
# Python 3.5.
module = types.ModuleType(name)
Expand Down Expand Up @@ -80,11 +81,17 @@ def test_importing_module_as_side_effect(self):
del sys.modules[data.__name__]
self.execute(data.__name__, 'utf-8.file')

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

def test_non_package_by_package(self):
# The anchor package cannot be a module.
with self.assertRaises(TypeError):
module = sys.modules['importlib_resources.tests.util']
self.execute(module, 'utf-8.file')

def test_resource_opener(self):
data = io.BytesIO(b'Hello, world!')
package = create_package(file=data, path=FileNotFoundError())
Expand Down
14 changes: 14 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import sys

from setuptools import setup


requirements = []
if sys.version_info < (3,):
requirements.append('pathlib2')


setup(
name='importlib_resources',
install_requires=requirements,
)
61 changes: 61 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[tox]
envlist = {py34,py35,py36,py37}-{nocov,cov,diffcov},qa
skip_missing_interpreters = True


[testenv]
commands =
nocov: python -m unittest discover
cov,diffcov: python -m coverage run {[coverage]rc} -m unittest discover {posargs}
cov,diffcov: python -m coverage combine {[coverage]rc}
cov: python -m coverage html {[coverage]rc}
cov: python -m coverage report -m {[coverage]rc} --fail-under=100
diffcov: python -m coverage xml {[coverage]rc}
diffcov: diff-cover coverage.xml --html-report diffcov.html
diffcov: diff-cover coverage.xml --fail-under=100
usedevelop = True
passenv =
PYTHON*
LANG*
LC_*
deps =
cov,diffcov: coverage
diffcov: diff_cover
setenv =
cov: COVERAGE_PROCESS_START={[coverage]rcfile}
cov: COVERAGE_OPTIONS="-p"
cov: COVERAGE_FILE={toxinidir}/.coverage
py34: GEVER=35
py35: GEVER=36
py36: GEVER=37
py36: LEVER=35


[testenv:qa]
basepython = python3
commands =
python -m flake8 importlib_resources
py34,py35,py36: mypy --ignore-missing-imports importlib_resources
deps =
py34,py35,py36: mypy
flake8


[testenv:docs]
basepython = python3
commands =
python setup.py build_sphinx
deps:
sphinx
docutils==0.12


[coverage]
rcfile = {toxinidir}/coverage.ini
rc = --rcfile={[coverage]rcfile}


[flake8]
hang-closing = True
jobs = 1
max-line-length = 79

0 comments on commit a07bc94

Please sign in to comment.