Commit 0cf9e6a6 authored by Brett Cannon's avatar Brett Cannon

Move importlib.abc.SourceLoader to _bootstrap.

Required updating code relying on other modules to switch to _bootstrap's
unique module requirements. This led to the realization that
get_code was being too liberal in its exception catching when calling set_data
by blindly grabbing IOError. Shifted the responsibility of safely ignoring
writes to a read-only path to set_data.

Importlib is still not relying on SourceLoader yet; requires creating a
SourcelessLoader and updating the source finder.
parent 9396483b
......@@ -248,7 +248,8 @@ are also provided to help in implementing the core ABCs.
.. method:: set_data(self, path, data)
Optional abstract method which writes the specified bytes to a file
path.
path. When writing to the path fails because the path is read-only, do
not propagate the exception.
.. method:: get_code(self, fullname)
......
......@@ -301,6 +301,143 @@ class FrozenImporter:
return imp.is_frozen_package(fullname)
class SourceLoader:
def path_mtime(self, path:str) -> int:
"""Optional method that returns the modification time for the specified
path.
Implementing this method allows the loader to read bytecode files.
"""
raise NotImplementedError
def set_data(self, path:str, data:bytes) -> None:
"""Optional method which writes data to a file path.
Implementing this method allows for the writing of bytecode files.
"""
raise NotImplementedError
def is_package(self, fullname):
"""Concrete implementation of InspectLoader.is_package by checking if
the path returned by get_filename has a filename of '__init__.py'."""
filename = self.get_filename(fullname).rsplit(path_sep, 1)[1]
return filename.rsplit('.', 1)[0] == '__init__'
def get_source(self, fullname):
"""Concrete implementation of InspectLoader.get_source."""
import tokenize
path = self.get_filename(fullname)
try:
source_bytes = self.get_data(path)
except IOError:
raise ImportError("source not available through get_data()")
encoding = tokenize.detect_encoding(_io.BytesIO(source_bytes).readline)
# XXX Universal newlines?
return source_bytes.decode(encoding[0])
def get_code(self, fullname):
"""Concrete implementation of InspectLoader.get_code.
Reading of bytecode requires path_mtime to be implemented. To write
bytecode, set_data must also be implemented.
"""
source_path = self.get_filename(fullname)
bytecode_path = imp.cache_from_source(source_path)
source_mtime = None
if bytecode_path is not None:
try:
source_mtime = self.path_mtime(source_path)
except NotImplementedError:
pass
else:
try:
data = self.get_data(bytecode_path)
except IOError:
pass
else:
magic = data[:4]
raw_timestamp = data[4:8]
if (len(magic) == 4 and len(raw_timestamp) == 4 and
magic == imp.get_magic() and
marshal._r_long(raw_timestamp) == source_mtime):
return marshal.loads(data[8:])
source_bytes = self.get_data(source_path)
code_object = compile(source_bytes, source_path, 'exec',
dont_inherit=True)
if (not sys.dont_write_bytecode and bytecode_path is not None and
source_mtime is not None):
# If e.g. Jython ever implements imp.cache_from_source to have
# their own cached file format, this block of code will most likely
# throw an exception.
data = bytearray(imp.get_magic())
data.extend(marshal._w_long(source_mtime))
data.extend(marshal.dumps(code_object))
try:
self.set_data(bytecode_path, data)
except NotImplementedError:
pass
return code_object
@module_for_loader
def load_module(self, module):
"""Concrete implementation of Loader.load_module.
Requires ExecutionLoader.get_filename and ResourceLoader.get_data to be
implemented to load source code. Use of bytecode is dictated by whether
get_code uses/writes bytecode.
"""
name = module.__name__
code_object = self.get_code(name)
module.__file__ = self.get_filename(name)
module.__cached__ = imp.cache_from_source(module.__file__)
module.__package__ = name
is_package = self.is_package(name)
if is_package:
module.__path__ = [module.__file__.rsplit(path_sep, 1)[0]]
else:
module.__package__ = module.__package__.rpartition('.')[0]
module.__loader__ = self
exec(code_object, module.__dict__)
return module
class _SourceFileLoader(SourceLoader):
"""Concrete implementation of SourceLoader.
NOT A PUBLIC CLASS! Do not expect any API stability from this class, so DO
NOT SUBCLASS IN YOUR OWN CODE!
"""
def __init__(self, fullname, path):
self._name = fullname
self._path = path
@_check_name
def get_filename(self, fullname):
"""Return the path to the source file as found by the finder."""
return self._path
def path_mtime(self, path):
"""Return the modification time for the path."""
return int(_os.stat(path).st_mtime)
def set_data(self, data, path):
"""Write bytes data to a file."""
try:
with _closing(_io.FileIO(bytecode_path, 'w')) as file:
file.write(data)
except IOError as exc:
if exc.errno != errno.EACCES:
raise
class PyLoader:
"""Loader base class for Python source code.
......
......@@ -100,7 +100,7 @@ class ExecutionLoader(InspectLoader):
raise NotImplementedError
class SourceLoader(ResourceLoader, ExecutionLoader):
class SourceLoader(_bootstrap.SourceLoader, ResourceLoader, ExecutionLoader):
"""Abstract base class for loading source code (and optionally any
corresponding bytecode).
......@@ -117,106 +117,6 @@ class SourceLoader(ResourceLoader, ExecutionLoader):
"""
def path_mtime(self, path:str) -> int:
"""Optional method that returns the modification time for the specified
path.
Implementing this method allows the loader to read bytecode files.
"""
raise NotImplementedError
def set_data(self, path:str, data:bytes) -> None:
"""Optional method which writes data to a file path.
Implementing this method allows for the writing of bytecode files.
"""
raise NotImplementedError
def is_package(self, fullname):
"""Concrete implementation of InspectLoader.is_package by checking if
the path returned by get_filename has a filename of '__init__.py'."""
filename = os.path.basename(self.get_filename(fullname))
return os.path.splitext(filename)[0] == '__init__'
def get_source(self, fullname):
"""Concrete implementation of InspectLoader.get_source."""
path = self.get_filename(fullname)
try:
source_bytes = self.get_data(path)
except IOError:
raise ImportError("source not available through get_data()")
encoding = tokenize.detect_encoding(io.BytesIO(source_bytes).readline)
return source_bytes.decode(encoding[0])
def get_code(self, fullname):
"""Concrete implementation of InspectLoader.get_code.
Reading of bytecode requires path_mtime to be implemented. To write
bytecode, set_data must also be implemented.
"""
source_path = self.get_filename(fullname)
bytecode_path = imp.cache_from_source(source_path)
source_mtime = None
if bytecode_path is not None:
try:
source_mtime = self.path_mtime(source_path)
except NotImplementedError:
pass
else:
try:
data = self.get_data(bytecode_path)
except IOError:
pass
else:
magic = data[:4]
raw_timestamp = data[4:8]
if (len(magic) == 4 and len(raw_timestamp) == 4 and
magic == imp.get_magic() and
marshal._r_long(raw_timestamp) == source_mtime):
return marshal.loads(data[8:])
source_bytes = self.get_data(source_path)
code_object = compile(source_bytes, source_path, 'exec',
dont_inherit=True)
if (not sys.dont_write_bytecode and bytecode_path is not None and
source_mtime is not None):
# If e.g. Jython ever implements imp.cache_from_source to have
# their own cached file format, this block of code will most likely
# throw an exception.
data = bytearray(imp.get_magic())
data.extend(marshal._w_long(source_mtime))
data.extend(marshal.dumps(code_object))
try:
self.set_data(bytecode_path, data)
except (NotImplementedError, IOError):
pass
return code_object
@util.module_for_loader
def load_module(self, module):
"""Concrete implementation of Loader.load_module.
Requires ExecutionLoader.get_filename and ResourceLoader.get_data to be
implemented to load source code. Use of bytecode is dictated by whether
get_code uses/writes bytecode.
"""
name = module.__name__
code_object = self.get_code(name)
module.__file__ = self.get_filename(name)
module.__cached__ = imp.cache_from_source(module.__file__)
module.__package__ = name
is_package = self.is_package(name)
if is_package:
module.__path__ = [os.path.dirname(module.__file__)]
else:
module.__package__ = module.__package__.rpartition('.')[0]
module.__loader__ = self
exec(code_object, module.__dict__)
return module
class PyLoader(SourceLoader):
......
......@@ -748,8 +748,7 @@ class SourceLoaderBytecodeTests(SourceLoaderTestHarness):
return closure
self.setUp(magic=b'0000')
for exc in (NotImplementedError, IOError):
self.loader.set_data = raise_exception(exc)
self.loader.set_data = raise_exception(NotImplementedError)
code_object = self.loader.get_code(self.name)
self.verify_code(code_object)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment