2020-05-08 20:20:26 -03:00
|
|
|
import os
|
|
|
|
import pathlib
|
|
|
|
import tempfile
|
|
|
|
import functools
|
|
|
|
import contextlib
|
2020-06-07 22:00:51 -03:00
|
|
|
import types
|
|
|
|
import importlib
|
2020-05-08 20:20:26 -03:00
|
|
|
|
2020-06-07 22:00:51 -03:00
|
|
|
from typing import Union, Any, Optional
|
2021-05-21 14:00:40 -03:00
|
|
|
from .abc import ResourceReader, Traversable
|
2020-05-08 20:20:26 -03:00
|
|
|
|
2021-03-04 14:43:00 -04:00
|
|
|
from ._adapters import wrap_spec
|
|
|
|
|
2020-06-07 22:00:51 -03:00
|
|
|
Package = Union[types.ModuleType, str]
|
2021-07-29 22:05:05 -03:00
|
|
|
Resource = Union[str, os.PathLike]
|
2020-06-07 22:00:51 -03:00
|
|
|
|
|
|
|
|
|
|
|
def files(package):
|
2021-05-21 14:00:40 -03:00
|
|
|
# type: (Package) -> Traversable
|
2020-05-08 20:20:26 -03:00
|
|
|
"""
|
2020-06-07 22:00:51 -03:00
|
|
|
Get a Traversable resource from a package
|
|
|
|
"""
|
|
|
|
return from_package(get_package(package))
|
|
|
|
|
2020-05-08 20:20:26 -03:00
|
|
|
|
2020-06-07 22:00:51 -03:00
|
|
|
def normalize_path(path):
|
|
|
|
# type: (Any) -> str
|
|
|
|
"""Normalize a path by ensuring it is a string.
|
|
|
|
|
|
|
|
If the resulting string contains path separators, an exception is raised.
|
2020-05-08 20:20:26 -03:00
|
|
|
"""
|
2020-06-07 22:00:51 -03:00
|
|
|
str_path = str(path)
|
|
|
|
parent, file_name = os.path.split(str_path)
|
|
|
|
if parent:
|
2021-05-26 17:16:11 -03:00
|
|
|
raise ValueError(f'{path!r} must be only a file name')
|
2020-06-07 22:00:51 -03:00
|
|
|
return file_name
|
2020-05-08 20:20:26 -03:00
|
|
|
|
|
|
|
|
2020-06-07 22:00:51 -03:00
|
|
|
def get_resource_reader(package):
|
|
|
|
# type: (types.ModuleType) -> Optional[ResourceReader]
|
2020-05-08 20:20:26 -03:00
|
|
|
"""
|
2020-06-07 22:00:51 -03:00
|
|
|
Return the package's loader if it's a ResourceReader.
|
2020-05-08 20:20:26 -03:00
|
|
|
"""
|
2020-06-07 22:00:51 -03:00
|
|
|
# We can't use
|
|
|
|
# a issubclass() check here because apparently abc.'s __subclasscheck__()
|
|
|
|
# hook wants to create a weak reference to the object, but
|
|
|
|
# zipimport.zipimporter does not support weak references, resulting in a
|
|
|
|
# TypeError. That seems terrible.
|
|
|
|
spec = package.__spec__
|
2021-03-04 14:43:00 -04:00
|
|
|
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore
|
2020-06-07 22:00:51 -03:00
|
|
|
if reader is None:
|
|
|
|
return None
|
2021-03-04 14:43:00 -04:00
|
|
|
return reader(spec.name) # type: ignore
|
2020-05-08 20:20:26 -03:00
|
|
|
|
|
|
|
|
2020-06-07 22:00:51 -03:00
|
|
|
def resolve(cand):
|
|
|
|
# type: (Package) -> types.ModuleType
|
2021-03-04 14:43:00 -04:00
|
|
|
return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand)
|
2020-06-07 22:00:51 -03:00
|
|
|
|
|
|
|
|
|
|
|
def get_package(package):
|
|
|
|
# type: (Package) -> types.ModuleType
|
|
|
|
"""Take a package name or module object and return the module.
|
|
|
|
|
|
|
|
Raise an exception if the resolved module is not a package.
|
|
|
|
"""
|
|
|
|
resolved = resolve(package)
|
2021-03-04 14:43:00 -04:00
|
|
|
if wrap_spec(resolved).submodule_search_locations is None:
|
2021-05-26 17:16:11 -03:00
|
|
|
raise TypeError(f'{package!r} is not a package')
|
2020-06-07 22:00:51 -03:00
|
|
|
return resolved
|
|
|
|
|
|
|
|
|
|
|
|
def from_package(package):
|
|
|
|
"""
|
|
|
|
Return a Traversable object for the given package.
|
|
|
|
|
|
|
|
"""
|
2021-03-04 14:43:00 -04:00
|
|
|
spec = wrap_spec(package)
|
2020-06-07 22:00:51 -03:00
|
|
|
reader = spec.loader.get_resource_reader(spec.name)
|
|
|
|
return reader.files()
|
2020-05-08 20:20:26 -03:00
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def _tempfile(reader, suffix=''):
|
|
|
|
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
|
|
|
|
# blocks due to the need to close the temporary file to work on Windows
|
|
|
|
# properly.
|
|
|
|
fd, raw_path = tempfile.mkstemp(suffix=suffix)
|
|
|
|
try:
|
2021-07-30 21:37:09 -03:00
|
|
|
try:
|
|
|
|
os.write(fd, reader())
|
|
|
|
finally:
|
|
|
|
os.close(fd)
|
2020-10-25 15:21:46 -03:00
|
|
|
del reader
|
2020-05-08 20:20:26 -03:00
|
|
|
yield pathlib.Path(raw_path)
|
|
|
|
finally:
|
|
|
|
try:
|
|
|
|
os.remove(raw_path)
|
2021-07-30 21:37:09 -03:00
|
|
|
except FileNotFoundError:
|
2020-05-08 20:20:26 -03:00
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@functools.singledispatch
|
|
|
|
def as_file(path):
|
|
|
|
"""
|
|
|
|
Given a Traversable object, return that object as a
|
|
|
|
path on the local file system in a context manager.
|
|
|
|
"""
|
2020-10-25 15:21:46 -03:00
|
|
|
return _tempfile(path.read_bytes, suffix=path.name)
|
2020-05-08 20:20:26 -03:00
|
|
|
|
|
|
|
|
|
|
|
@as_file.register(pathlib.Path)
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def _(path):
|
|
|
|
"""
|
|
|
|
Degenerate behavior for pathlib.Path objects.
|
|
|
|
"""
|
|
|
|
yield path
|