Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions Lib/importlib/_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from contextlib import suppress

from . import abc


class SpecLoaderAdapter:
"""
Adapt a package spec to adapt the underlying loader.
"""

def __init__(self, spec, adapter=lambda spec: spec.loader):
self.spec = spec
self.loader = adapter(spec)

def __getattr__(self, name):
return getattr(self.spec, name)


class TraversableResourcesLoader:
"""
Adapt a loader to provide TraversableResources.
"""

def __init__(self, spec):
self.spec = spec

def get_resource_reader(self, name):
return DegenerateFiles(self.spec)._native()


class DegenerateFiles:
"""
Adapter for an existing or non-existant resource reader
to provide a degenerate .files().
"""

class Path(abc.Traversable):
def iterdir(self):
return iter(())

def is_dir(self):
return False

is_file = exists = is_dir # type: ignore

def joinpath(self, other):
return DegenerateFiles.Path()

def name(self):
return ''

def open(self):
raise ValueError()

def __init__(self, spec):
self.spec = spec

@property
def _reader(self):
with suppress(AttributeError):
return self.spec.loader.get_resource_reader(self.spec.name)

def _native(self):
"""
Return the native reader if it supports files().
"""
reader = self._reader
return reader if hasattr(reader, 'files') else self

def __getattr__(self, attr):
return getattr(self._reader, attr)

def files(self):
return DegenerateFiles.Path()


def wrap_spec(package):
"""
Construct a package spec with traversable compatibility
on the spec/loader/reader.
"""
return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader)
4 changes: 4 additions & 0 deletions Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,10 @@ def load_module(self, fullname):
# Warning implemented in _load_module_shim().
return _bootstrap._load_module_shim(self, fullname)

def get_resource_reader(self, module):
from importlib.readers import NamespaceReader
return NamespaceReader(self._path)


# Finders #####################################################################

Expand Down
15 changes: 7 additions & 8 deletions Lib/importlib/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from typing import Union, Any, Optional
from .abc import ResourceReader

from ._adapters import wrap_spec

Package = Union[types.ModuleType, str]


Expand Down Expand Up @@ -43,18 +45,15 @@ def get_resource_reader(package):
# zipimport.zipimporter does not support weak references, resulting in a
# TypeError. That seems terrible.
spec = package.__spec__
reader = getattr(spec.loader, 'get_resource_reader', None)
reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore
if reader is None:
return None
return reader(spec.name)
return reader(spec.name) # type: ignore


def resolve(cand):
# type: (Package) -> types.ModuleType
return (
cand if isinstance(cand, types.ModuleType)
else importlib.import_module(cand)
)
return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand)


def get_package(package):
Expand All @@ -64,7 +63,7 @@ def get_package(package):
Raise an exception if the resolved module is not a package.
"""
resolved = resolve(package)
if resolved.__spec__.submodule_search_locations is None:
if wrap_spec(resolved).submodule_search_locations is None:
raise TypeError('{!r} is not a package'.format(package))
return resolved

Expand All @@ -74,7 +73,7 @@ def from_package(package):
Return a Traversable object for the given package.

"""
spec = package.__spec__
spec = wrap_spec(package)
reader = spec.loader.get_resource_reader(spec.name)
return reader.files()

Expand Down
65 changes: 34 additions & 31 deletions Lib/importlib/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from ._abc import Loader
import abc
import warnings
from typing import BinaryIO, Iterable, Text
from typing import Protocol, runtime_checkable


Expand Down Expand Up @@ -297,49 +298,45 @@ def set_data(self, path, data):


class ResourceReader(metaclass=abc.ABCMeta):

"""Abstract base class to provide resource-reading support.

Loaders that support resource reading are expected to implement
the ``get_resource_reader(fullname)`` method and have it either return None
or an object compatible with this ABC.
"""
"""Abstract base class for loaders to provide resource reading support."""

@abc.abstractmethod
def open_resource(self, resource):
def open_resource(self, resource: Text) -> BinaryIO:
"""Return an opened, file-like object for binary reading.

The 'resource' argument is expected to represent only a file name
and thus not contain any subdirectory components.

The 'resource' argument is expected to represent only a file name.
If the resource cannot be found, FileNotFoundError is raised.
"""
# This deliberately raises FileNotFoundError instead of
# NotImplementedError so that if this method is accidentally called,
# it'll still do the right thing.
raise FileNotFoundError

@abc.abstractmethod
def resource_path(self, resource):
def resource_path(self, resource: Text) -> Text:
"""Return the file system path to the specified resource.

The 'resource' argument is expected to represent only a file name
and thus not contain any subdirectory components.

The 'resource' argument is expected to represent only a file name.
If the resource does not exist on the file system, 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

@abc.abstractmethod
def is_resource(self, name):
"""Return True if the named 'name' is consider a resource."""
def is_resource(self, path: Text) -> bool:
"""Return True if the named 'path' is a resource.

Files are resources, directories are not.
"""
raise FileNotFoundError

@abc.abstractmethod
def contents(self):
"""Return an iterable of strings over the contents of the package."""
return []


_register(ResourceReader, machinery.SourceFileLoader)
def contents(self) -> Iterable[str]:
"""Return an iterable of entries in `package`."""
raise FileNotFoundError


@runtime_checkable
Expand All @@ -355,26 +352,28 @@ def iterdir(self):
Yield Traversable objects in self
"""

@abc.abstractmethod
def read_bytes(self):
"""
Read contents of self as bytes
"""
with self.open('rb') as strm:
return strm.read()

@abc.abstractmethod
def read_text(self, encoding=None):
"""
Read contents of self as bytes
Read contents of self as text
"""
with self.open(encoding=encoding) as strm:
return strm.read()

@abc.abstractmethod
def is_dir(self):
def is_dir(self) -> bool:
"""
Return True if self is a dir
"""

@abc.abstractmethod
def is_file(self):
def is_file(self) -> bool:
"""
Return True if self is a file
"""
Expand All @@ -385,11 +384,11 @@ def joinpath(self, child):
Return Traversable child in self
"""

@abc.abstractmethod
def __truediv__(self, child):
"""
Return Traversable child in self
"""
return self.joinpath(child)

@abc.abstractmethod
def open(self, mode='r', *args, **kwargs):
Expand All @@ -402,14 +401,18 @@ def open(self, mode='r', *args, **kwargs):
"""

@abc.abstractproperty
def name(self):
# type: () -> str
def name(self) -> str:
"""
The base name of this object without any parent references.
"""


class TraversableResources(ResourceReader):
"""
The required interface for providing traversable
resources.
"""

@abc.abstractmethod
def files(self):
"""Return a Traversable object for the loaded package."""
Expand Down
82 changes: 82 additions & 0 deletions Lib/importlib/readers.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import collections
import zipfile
import pathlib
from . import abc


def remove_duplicates(items):
return iter(collections.OrderedDict.fromkeys(items))


class FileReader(abc.TraversableResources):
def __init__(self, loader):
self.path = pathlib.Path(loader.path).parent
Expand Down Expand Up @@ -39,3 +44,80 @@ def is_resource(self, path):

def files(self):
return zipfile.Path(self.archive, self.prefix)


class MultiplexedPath(abc.Traversable):
"""
Given a series of Traversable objects, implement a merged
version of the interface across all objects. Useful for
namespace packages which may be multihomed at a single
name.
"""

def __init__(self, *paths):
self._paths = list(map(pathlib.Path, remove_duplicates(paths)))
if not self._paths:
message = 'MultiplexedPath must contain at least one path'
raise FileNotFoundError(message)
if not all(path.is_dir() for path in self._paths):
raise NotADirectoryError('MultiplexedPath only supports directories')

def iterdir(self):
visited = []
for path in self._paths:
for file in path.iterdir():
if file.name in visited:
continue
visited.append(file.name)
yield file

def read_bytes(self):
raise FileNotFoundError(f'{self} is not a file')

def read_text(self, *args, **kwargs):
raise FileNotFoundError(f'{self} is not a file')

def is_dir(self):
return True

def is_file(self):
return False

def joinpath(self, child):
# first try to find child in current paths
for file in self.iterdir():
if file.name == child:
return file
# if it does not exist, construct it with the first path
return self._paths[0] / child

__truediv__ = joinpath

def open(self, *args, **kwargs):
raise FileNotFoundError('{} is not a file'.format(self))

def name(self):
return self._paths[0].name

def __repr__(self):
return 'MultiplexedPath({})'.format(
', '.join("'{}'".format(path) for path in self._paths)
)


class NamespaceReader(abc.TraversableResources):
def __init__(self, namespace_path):
if 'NamespacePath' not in str(namespace_path):
raise ValueError('Invalid path')
self.path = MultiplexedPath(*list(namespace_path))

def resource_path(self, resource):
"""
Return the file system path to prevent
`resources.path()` from creating a temporary
copy.
"""
return str(self.path.joinpath(resource))

def files(self):
return self.path
Loading