Skip to content

Commit 64a9e9f

Browse files
authored
Support Python 2.7 (python#24)
Refactor into Python 3 and Python 2.7 submodules.
1 parent e11c236 commit 64a9e9f

File tree

9 files changed

+351
-170
lines changed

9 files changed

+351
-170
lines changed

coverage.ini

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ branch = true
33
parallel = true
44
omit =
55
setup*
6-
.tox/*/lib/python3.*/site-packages/*
7-
.tox/*/lib/python3.*/site-packages/*
6+
.tox/*/lib/python*/site-packages/*
87
*/tests/*.py
98
/tmp/*
109
/private/var/folders/*
1110
*/testing/*.py
11+
importlib_resources/_py${OMIT}.py
12+
importlib_resources/__init__.py
13+
importlib_resources/_compat.py
14+
importlib_resources/abc.py
1215

1316
[report]
1417
exclude_lines =

importlib_resources/__init__.py

Lines changed: 4 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,11 @@
11
"""Read resources contained within a package."""
2-
__version__ = '0.1.0'
32

4-
import builtins
5-
import contextlib
6-
import importlib
7-
import importlib.abc
8-
import io
9-
import os.path
10-
import pathlib
113
import sys
12-
import tempfile
13-
import types
14-
import typing
15-
from typing import Iterator, Union
16-
from typing.io import BinaryIO
174

18-
from . import abc as resources_abc
5+
__version__ = '0.1.0'
196

207

21-
Package = Union[types.ModuleType, str]
22-
if sys.version_info >= (3, 6):
23-
FileName = Union[str, os.PathLike] # pragma: ge36
8+
if sys.version_info >= (3,):
9+
from importlib_resources._py3 import open, path, read
2410
else:
25-
FileName = str # pragma: le35
26-
27-
28-
def _get_package(package) -> types.ModuleType:
29-
if hasattr(package, '__spec__'):
30-
if package.__spec__.submodule_search_locations is None:
31-
raise TypeError("{!r} is not a package".format(
32-
package.__spec__.name))
33-
else:
34-
return package
35-
else:
36-
module = importlib.import_module(package)
37-
if module.__spec__.submodule_search_locations is None:
38-
raise TypeError("{!r} is not a package".format(package))
39-
else:
40-
return module
41-
42-
43-
def _normalize_path(path) -> str:
44-
str_path = str(path)
45-
parent, file_name = os.path.split(str_path)
46-
if parent:
47-
raise ValueError("{!r} must be only a file name".format(path))
48-
else:
49-
return file_name
50-
51-
52-
def open(package: Package, file_name: FileName) -> BinaryIO:
53-
"""Return a file-like object opened for binary-reading of the resource."""
54-
file_name = _normalize_path(file_name)
55-
package = _get_package(package)
56-
if hasattr(package.__spec__.loader, 'open_resource'):
57-
reader = typing.cast(resources_abc.ResourceReader,
58-
package.__spec__.loader)
59-
return reader.open_resource(file_name)
60-
else:
61-
# Using pathlib doesn't work well here due to the lack of 'strict'
62-
# argument for pathlib.Path.resolve() prior to Python 3.6.
63-
absolute_package_path = os.path.abspath(package.__spec__.origin)
64-
package_path = os.path.dirname(absolute_package_path)
65-
full_path = os.path.join(package_path, file_name)
66-
try:
67-
return builtins.open(full_path, 'rb')
68-
except IOError:
69-
# Just assume the loader is a resource loader; all the relevant
70-
# importlib.machinery loaders are and an AttributeError for
71-
# get_data() will make it clear what is needed from the loader.
72-
loader = typing.cast(importlib.abc.ResourceLoader,
73-
package.__spec__.loader)
74-
try:
75-
data = loader.get_data(full_path)
76-
except IOError:
77-
package_name = package.__spec__.name
78-
message = '{!r} resource not found in {!r}'.format(
79-
file_name, package_name)
80-
raise FileNotFoundError(message)
81-
else:
82-
return io.BytesIO(data)
83-
84-
85-
def read(package: Package, file_name: FileName, encoding: str = 'utf-8',
86-
errors: str = 'strict') -> str:
87-
"""Return the decoded string of the resource.
88-
89-
The decoding-related arguments have the same semantics as those of
90-
bytes.decode().
91-
"""
92-
file_name = _normalize_path(file_name)
93-
package = _get_package(package)
94-
# Note this is **not** builtins.open()!
95-
with open(package, file_name) as binary_file:
96-
# Decoding from io.TextIOWrapper() instead of str.decode() in hopes
97-
# that the former will be smarter about memory usage.
98-
text_file = io.TextIOWrapper(binary_file, encoding=encoding,
99-
errors=errors)
100-
return text_file.read()
101-
102-
103-
@contextlib.contextmanager
104-
def path(package: Package, file_name: FileName) -> Iterator[pathlib.Path]:
105-
"""A context manager providing a file path object to the resource.
106-
107-
If the resource does not already exist on its own on the file system,
108-
a temporary file will be created. If the file was created, the file
109-
will be deleted upon exiting the context manager (no exception is
110-
raised if the file was deleted prior to the context manager
111-
exiting).
112-
"""
113-
file_name = _normalize_path(file_name)
114-
package = _get_package(package)
115-
if hasattr(package.__spec__.loader, 'resource_path'):
116-
reader = typing.cast(resources_abc.ResourceReader,
117-
package.__spec__.loader)
118-
try:
119-
yield pathlib.Path(reader.resource_path(file_name))
120-
return
121-
except FileNotFoundError:
122-
pass
123-
# Fall-through for both the lack of resource_path() *and* if
124-
# resource_path() raises FileNotFoundError.
125-
package_directory = pathlib.Path(package.__spec__.origin).parent
126-
file_path = package_directory / file_name
127-
if file_path.exists():
128-
yield file_path
129-
else:
130-
with open(package, file_name) as file:
131-
data = file.read()
132-
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
133-
# blocks due to the need to close the temporary file to work on
134-
# Windows properly.
135-
fd, raw_path = tempfile.mkstemp()
136-
try:
137-
os.write(fd, data)
138-
os.close(fd)
139-
yield pathlib.Path(raw_path)
140-
finally:
141-
try:
142-
os.remove(raw_path)
143-
except FileNotFoundError:
144-
pass
11+
from importlib_resources._py2 import open, path, read

importlib_resources/_compat.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from __future__ import absolute_import
2+
3+
# flake8: noqa
4+
5+
try:
6+
from pathlib import Path, PurePath
7+
except ImportError:
8+
from pathlib2 import Path, PurePath # type: ignore
9+
10+
11+
try:
12+
from abc import ABC # type: ignore
13+
except ImportError:
14+
from abc import ABCMeta
15+
16+
class ABC(object): # type: ignore
17+
__metaclass__ = ABCMeta
18+
19+
20+
try:
21+
FileNotFoundError = FileNotFoundError # type: ignore
22+
except NameError:
23+
FileNotFoundError = OSError

importlib_resources/_py2.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import os
2+
import tempfile
3+
4+
from ._compat import FileNotFoundError
5+
from __builtin__ import open as builtin_open
6+
from contextlib import contextmanager
7+
from importlib import import_module
8+
from io import BytesIO
9+
from pathlib2 import Path
10+
11+
12+
def _get_package(package):
13+
# `package` will be a string or a module. Always return a module which is
14+
# a package, otherwise raise an exception.
15+
if isinstance(package, basestring): # noqa: F821
16+
module = import_module(package)
17+
else:
18+
module = package
19+
if not hasattr(module, '__path__'):
20+
raise TypeError("{!r} is not a package".format(package))
21+
return module
22+
23+
24+
def _normalize_path(path):
25+
# Ensure that the incoming `path`, which may be a string or a Path object,
26+
# is a bare file name with no hierarchy.
27+
str_path = str(path)
28+
parent, file_name = os.path.split(str_path)
29+
if parent:
30+
raise ValueError("{!r} must be only a file name".format(path))
31+
else:
32+
return file_name
33+
34+
35+
def open(package, file_name):
36+
"""Return a file-like object opened for binary-reading of the resource."""
37+
file_name = _normalize_path(file_name)
38+
package = _get_package(package)
39+
# Using pathlib doesn't work well here due to the lack of 'strict' argument
40+
# for pathlib.Path.resolve() prior to Python 3.6.
41+
package_path = os.path.dirname(package.__file__)
42+
relative_path = os.path.join(package_path, file_name)
43+
full_path = os.path.abspath(relative_path)
44+
try:
45+
return builtin_open(full_path, 'rb')
46+
except IOError:
47+
# This might be a package in a zip file. zipimport provides a loader
48+
# with a functioning get_data() method, however we have to strip the
49+
# archive (i.e. the .zip file's name) off the front of the path. This
50+
# is because the zipimport loader in Python 2 doesn't actually follow
51+
# PEP 302. It should allow the full path, but actually requires that
52+
# the path be relative to the zip file.
53+
try:
54+
loader = package.__loader__
55+
full_path = relative_path[len(loader.archive)+1:]
56+
data = loader.get_data(full_path)
57+
except (IOError, AttributeError):
58+
package_name = package.__name__
59+
message = '{!r} resource not found in {!r}'.format(
60+
file_name, package_name)
61+
raise FileNotFoundError(message)
62+
else:
63+
return BytesIO(data)
64+
65+
66+
def read(package, file_name, encoding='utf-8', errors='strict'):
67+
"""Return the decoded string of the resource.
68+
69+
The decoding-related arguments have the same semantics as those of
70+
bytes.decode().
71+
"""
72+
file_name = _normalize_path(file_name)
73+
package = _get_package(package)
74+
# Note this is **not** builtins.open()!
75+
with open(package, file_name) as binary_file:
76+
return binary_file.read().decode(encoding=encoding, errors=errors)
77+
78+
79+
@contextmanager
80+
def path(package, file_name):
81+
"""A context manager providing a file path object to the resource.
82+
83+
If the resource does not already exist on its own on the file system,
84+
a temporary file will be created. If the file was created, the file
85+
will be deleted upon exiting the context manager (no exception is
86+
raised if the file was deleted prior to the context manager
87+
exiting).
88+
"""
89+
file_name = _normalize_path(file_name)
90+
package = _get_package(package)
91+
package_directory = Path(package.__file__).parent
92+
file_path = package_directory / file_name
93+
# If the file actually exists on the file system, just return it.
94+
# Otherwise, it's probably in a zip file, so we need to create a temporary
95+
# file and copy the contents into that file, hence the contextmanager to
96+
# clean up the temp file resource.
97+
if file_path.exists():
98+
yield file_path
99+
else:
100+
# Note this is **not** builtins.open()!
101+
with open(package, file_name) as fileobj:
102+
data = fileobj.read()
103+
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
104+
# blocks due to the need to close the temporary file to work on Windows
105+
# properly.
106+
fd, raw_path = tempfile.mkstemp()
107+
try:
108+
os.write(fd, data)
109+
os.close(fd)
110+
yield Path(raw_path)
111+
finally:
112+
try:
113+
os.remove(raw_path)
114+
except FileNotFoundError:
115+
pass

0 commit comments

Comments
 (0)