Re: PEP proposal: Per-Module Import Path
Eric Snow <[email protected]>
| Newsgroups | gmane.comp.python.import |
|---|---|
| Message-ID | <CALFfu7CovCS_u_vs8TjGZ7X7R-5pUgbkkL8W0bkF4coN6ioKYQ@mail.gmail.com> |
On Thu, Aug 1, 2013 at 6:56 PM, Nick Coghlan <[email protected]> wrote: > > On 2 Aug 2013 00:44, "Eric Snow" <[email protected]> wrote: > > This is pretty much exactly what I've been thinking about since PyCon. > The only difference is that I have a distinct ModuleSpec class and modules > would get a new __spec__ attribute. > > And we can quit adding ever more magic attributes directly to the module > namespace. I like it. > Yeah, that was part of what lead me to the idea. This could be taken to some pretty great lengths (I've given it a lot of thought), but I'm trying hard to not do too much at once. I wasn't even planning on pursuing ModuleSpec until 3.5, much less any of my more drastic ideas. > With that model, things might look vaguely like: > > 1. Finders would optionally offer "get_module_spec" (although a better > name would be nice!) > How about "find_module"? <.5 wink> Actually, I'm pretty sure this can be done in a backward-compatible way (in not too much time I've roughed out an implementation that should work). I would rather not introduce more API to the import system, but if that's preferable to hijacking (or improving <wink>) find_module() then I can live with that. However, given the crowd that takes advantage of the import system APIs, I wouldn't consider the change disruptive as long as it's backward compatible. This would also allow us to deprecate PathEntryFinder.get_loader() which we wouldn't have needed if we'd had something like ModuleSpec. > 2. Specs would have a load() method for the import system to call that > optionally accepted an existing module object (this would then cover > reload). > That's been my plan from the get-go. Good call on the reload case. > 3. The responsibility for checking the sys.modules cache would move to the > import system. > To me it makes sense to go even further. ModuleSpec could easily take over a bunch of the responsibilities of loaders, particularly related to the management of the module objects. Also, Loader.init_module_attrs() and importlib.util.module_to_load() could be pulled before the 3.4 release (since they are new in 3.4). It would stink if we found we no longer needed them after they get locked in by the release. Note, however, that they can co-exist with ModuleSpec just fine so it's not as big a deal. > 4. We'd create a "SpecLoader" to offer backwards compatibility in the old > __loader__ attribute. > Interesting. I had anticipated loaders still sticking around, still exposed by module.__loader__ and filling most of their current role, especially with regard to the optional PEP 302 APIs. I suppose we could deprecate the __loader__ attribute, and maybe even __package__, in favor of __spec__, but I don't think there's any rush to do so before Python 4000. > Slight(!) tangent from the original problem, but a worthwhile refactoring > issue to tackle, I think :) > Yeah, even if it proves too big a change for 3.4 and we take some other approach for indirections, I think there's a lot to gain from separating the module specification from the module and from the loader. I've attached a patch that does the bare minimum of what I think we'd want from ModuleSpec. I'll probably flesh out more of my ideas for it later. Of course, I don't want anything here to get in the way of the .ref PEP which I think has more concrete value. So if this tangent threatens any chance at getting indirection files for 3.4, I'd rather defer any effort on these extras until 3.5 in favor of a simpler (if less desirable) approach. -eric _______________________________________________ Import-SIG mailing list [email protected] http://mail.python.org/mailman/listinfo/import-sig
modulespec.diff
(application/octet-stream, 9.6 KB)
# HG changeset patch
# Parent db9fe49069edccee480196978e670968e271d2e8
Add ModuleSpec.
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -1476,6 +1476,203 @@
return 'FileFinder({!r})'.format(self.path)
+# Module specifications #######################################################
+
+class ModuleSpec:
+ # XXX Factor out filename/cached/path into separate PathModuleSpec class?
+
+ @classmethod
+ def from_module(cls, module):
+ if isinstance(module, str):
+ module = sys.modules[module]
+ return cls(module.__name__,
+ module.__loader__,
+ module.__package__,
+ getattr(module, '__file__', None),
+ getattr(module, '__cached__', None),
+ getattr(module, '__path__', None))
+
+ def __init__(self, name, loader=None, package=None, filename=None,
+ cached=None, path=None):
+ self.name = name
+ self.loader = loader
+ self._package = package
+ self._filename = filename
+ self.cached = cached
+ self.path = path
+
+ # XXX Add 'paths' list containing chain of traversed paths?
+
+ def __repr__(self):
+ attrs = ', '.join('{}={!r}'.format(name, getattr(self, name))
+ for name in ['name', 'loader', 'package',
+ 'filename', 'cached', 'path']
+ if getattr(self, name) is not None)
+ return '{}({})'.format(self.__class__.__name__, attrs)
+
+ @property
+ def package(self):
+ if self._package is not None:
+ return self._package
+ elif self.is_package:
+ return self.name
+ else:
+ return self.name.rpartition('.')[0]
+
+ @package.setter
+ def package(self, package):
+ self._package = package
+
+ @property
+ def filename(self):
+ if self._filename is None:
+ try:
+ get_filename = self.loader.get_filename
+ except AttributeError:
+ pass
+ else:
+ self._filename = get_filename(self.name)
+ return self._filename
+
+ @filename.setter
+ def filename(self, filename):
+ self._filename = filename
+
+ # XXX Comflicts with backward-compatibility.
+ #@property
+ #def is_package(self):
+ # try:
+ # is_package = self.loader.is_package
+ # except AttributeError:
+ # return self.path is not None
+ # else:
+ # return is_package(self.name)
+
+ def copy(self):
+ # XXX Should SimpleNamespace get this method?
+ # XXX Allow **kwargs for overrides?
+ copied = self.__class__(self.name)
+ copied.__original__ = self
+ vars(copied).update(vars(self))
+ return copied
+
+ def load(self):
+ # XXX Add 'module' param to support reloading.
+ # XXX Add 'match_attrs' param? spec attrs would match module
+ # attrs (if set).
+ # XXX Let load() optionally add modules to sys.modules, rather
+ # than the loader.
+
+ if self.name not in sys.modules:
+ # ignore the returned module due to backward compatibility
+ self.loader.load_module(self.name)
+ _verbose_message('import {!r} # {!r}', self.name, self.loader)
+ # Backwards-compatibility; be nicer to skip the dict lookup.
+ module = sys.modules[self.name]
+ copied_spec = self.copy()
+ module.__spec__ = copied_spec
+
+# # set required attributes, if not already set
+# for attr in ['name', 'package', 'loader']:
+# spec_attr = attr
+# module_attr = "__{}__".format(attr)
+# current = getattr(module, module_attr, None)
+# if current is None:
+# spec_value = getattr(self, spec_attr)
+# try:
+# setattr(module, module_attr, spec_value)
+# except AttributeError:
+# pass
+# else:
+# setattr(copied_spec, module_attr, current)
+#
+# # set optional attributes, if not already set
+# for attr in ['file', 'cached', 'path']:
+# spec_attr = attr if attr != 'file' else 'filename'
+# module_attr = "__{}__".format(attr)
+# current = getattr(module, module_attr, None)
+# if current is None:
+# spec_value = getattr(self, spec_attr)
+# if spec_value is not None:
+# try:
+# setattr(module, module_attr, spec_value)
+# except AttributeError:
+# pass
+# else:
+# setattr(spec, module_attr, current)
+
+
+ # Set the required attributes, if not already set.
+
+ if getattr(module, '__package__', None) is None:
+ try:
+ module.__package__ = self.package
+ except AttributeError:
+ pass
+ elif module.__package__ != self.package:
+ copied_spec.package = module.__package__
+
+ if getattr(module, '__loader__', None) is None:
+ try:
+ module.__loader__ = self.loader
+ except AttributeError:
+ pass
+ elif module.__loader__ != self.loader:
+ copied_spec.loader = module.__loader__
+
+ # Set the optional module attributes.
+
+ if getattr(module, '__file__', None) is None:
+ if self.filename is not None:
+ try:
+ module.__file__ = self.filename
+ except AttributeError:
+ pass
+ elif module.__file__ != self.filename:
+ copied_spec.filename = module.__file__
+
+ if getattr(module, '__cached__', None) is None:
+ if self.cached is not None:
+ try:
+ module.__cached__ = self.cached
+ except AttributeError:
+ pass
+ elif module.__cached__ != self.cached:
+ copied_spec.cached = module.__cached__
+
+ if getattr(module, '__path__', None) is None:
+ if self.path is not None:
+ try:
+ module.__path__ = self.filename
+ except AttributeError:
+ pass
+ elif module.__path__ is not None:
+ copied_spec.path = module.__path__
+
+ return module
+
+ def get_data(self, path):
+ return self.loader.get_data(path)
+
+ def set_data(self, path, data):
+ return self.loader.set_data(path, data)
+
+ # XXX Comflicts with backward-compatibility.
+ #def get_source(self):
+ # return self.loader.get_code(self.name)
+
+ # XXX Comflicts with backward-compatibility.
+ #def get_code(self):
+ # return self.loader.get_code(self.name)
+
+
+class _BackwardCompatibleModuleSpec(ModuleSpec):
+ # Proxies its loader.
+
+ def __getattr__(self, name):
+ return getattr(self.loader, name)
+
+
# Import itself ###############################################################
class _ImportLockContext:
@@ -1506,13 +1703,16 @@
_warnings.warn('sys.meta_path is empty', ImportWarning)
for finder in sys.meta_path:
with _ImportLockContext():
- loader = finder.find_module(name, path)
- if loader is not None:
+ spec = finder.find_module(name, path)
+ if spec is not None:
# The parent import may have already imported this module.
if name not in sys.modules:
- return loader
+ if not isinstance(spec, ModuleSpec):
+ loader = spec
+ spec = _BackwardCompatibleModuleSpec(name, loader, path)
+ return spec
else:
- return sys.modules[name].__loader__
+ return sys.modules[name].__spec__
else:
return None
@@ -1553,33 +1753,15 @@
except AttributeError:
msg = (_ERR_MSG + '; {} is not a package').format(name, parent)
raise ImportError(msg, name=name)
- loader = _find_module(name, path)
- if loader is None:
+ spec = _find_module(name, path)
+ if spec is None:
raise ImportError(_ERR_MSG.format(name), name=name)
- elif name not in sys.modules:
- # The parent import may have already imported this module.
- loader.load_module(name)
- _verbose_message('import {!r} # {!r}', name, loader)
- # Backwards-compatibility; be nicer to skip the dict lookup.
- module = sys.modules[name]
+ else:
+ module = spec.load()
if parent:
# Set the module as an attribute on its parent.
parent_module = sys.modules[parent]
setattr(parent_module, name.rpartition('.')[2], module)
- # Set __package__ if the loader did not.
- if getattr(module, '__package__', None) is None:
- try:
- module.__package__ = module.__name__
- if not hasattr(module, '__path__'):
- module.__package__ = module.__package__.rpartition('.')[0]
- except AttributeError:
- pass
- # Set loader if need be.
- if getattr(module, '__loader__', None) is None:
- try:
- module.__loader__ = loader
- except AttributeError:
- pass
return module
@@ -1789,6 +1971,8 @@
if '_d.pyd' in EXTENSION_SUFFIXES:
WindowsRegistryFinder.DEBUG_BUILD = True
+ # XXX where should __spec__ be set for builtin/frozen modules?
+
def _install(sys_module, _imp_module):
"""Install importlib as the implementation of import."""