Source code for xotl.tools.modules

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ---------------------------------------------------------------------
# Copyright (c) Merchise Autrement [~º/~] and Contributors
# All rights reserved.
#
# This is free software; you can do what the LICENCE file allows you to.
#

"""Modules utilities."""

from types import ModuleType

# TODO: Implement the concept of module descriptor


[docs]def force_module(ref=None): """Load a module from a string or return module if already created. If `ref` is not specified (or integer) calling module is assumed looking in the stack. .. note:: Implementation detail Function used to inspect the stack is not guaranteed to exist in all implementations of Python. """ from importlib import import_module if isinstance(ref, ModuleType): return ref else: if ref is None: ref = 1 if isinstance(ref, int): import sys frame = sys._getframe(ref) try: ref = frame.f_globals["__name__"] finally: # As recommended to avoid memory leaks del frame if not isinstance(ref, str): if isinstance(ref, bytes): ref = ref.decode() # Python 3.x else: try: ref = ref.encode() # Python 2.x except Exception: # TODO: @med which exceptions expected? msg = "invalid type '{}' for module name '{}'" raise TypeError(msg.format(type(ref).__name__, ref)) return import_module(ref)
# TODO: Deprecate this method in favor of ``from <module> import *``
[docs]def copy_members(source=None, target=None): """Copy module members from `source` to `target`. It's common in `xotl.tools` package to extend Python modules with the same name, for example `xotl.tools.datetime` has all public members of Python's `datetime`. `copy_members`:func: can be used to copy all members from the original module to the extended one. :param source: string with source module name or module itself. If not given, is assumed as the last module part name of `target`. :param target: string with target module name or module itself. If not given, target name is looked in the stack of caller module. :returns: Source module. :rtype: `ModuleType` .. warning:: Implementation detail Function used to inspect the stack is not guaranteed to exist in all implementations of Python. """ target = force_module(target or 2) if source is None: source = target.__name__.rsplit(".")[-1] if source == target.__name__: msg = '"source" and "target" modules must be different.' raise ValueError(msg) source = force_module(source) for attr in dir(source): if not attr.startswith("__"): setattr(target, attr, getattr(source, attr)) return source
class _CustomModuleBase(ModuleType): pass
[docs]def customize(module, custom_attrs=None, meta=None): """Replaces a `module` by a custom one. Injects all kwargs into the newly created module's class. This allows to have module into which we may have properties or other type of descriptors. :param module: The module object to customize. :param custom_attrs: A dictionary of custom attributes that should be injected in the customized module. .. versionadded:: 1.4.2 Changes the API, no longer uses the ``**kwargs`` idiom for custom attributes. :param meta: The metaclass of the module type. This should be a subclass of `type`. We will actually subclass this metaclass to properly inject `custom_attrs` in our own internal metaclass. :returns: A tuple of ``(module, customized, class)`` with the module in the first place, `customized` will be True only if the module was created (i.e `customize`:func: is idempotent), and the third item will be the class of the module (the first item). """ if not isinstance(module, _CustomModuleBase): import sys meta_base = meta if meta else type class CustomModuleType(meta_base): def __new__(cls, name, bases, attrs): if custom_attrs: attrs.update(custom_attrs) return super().__new__(cls, name, bases, attrs) class CustomModule(_CustomModuleBase, metaclass=CustomModuleType): def __getattr__(self, attr): self.__dict__[attr] = result = getattr(module, attr) return result def __dir__(self): res = set(dir(module)) if custom_attrs: res |= set(custom_attrs.keys()) return list(res) sys.modules[module.__name__] = result = CustomModule(module.__name__) return result, True, CustomModule else: return module, False, type(module)
[docs]def modulemethod(func): """Decorator that defines a module-level method. Simply a module-level method, will always receive a first argument `self` with the module object. """ import sys from functools import wraps self, _created, cls = customize(sys.modules[func.__module__]) @wraps(func) def inner(*args, **kwargs): return func(self, *args, **kwargs) setattr(cls, func.__name__, func) return inner
[docs]def moduleproperty(getter, setter=None, deleter=None, doc=None, base=property): """Decorator that creates a module-level property. The module of the `getter` is replaced by a custom implementation of the module, and the property is injected to the custom module's class. The parameter `base` serves the purpose of changing the base for the property. For instance, this allows you to have `memoized_properties <xotl.tools.objects.memoized_property>`:func: at the module-level:: def memoized(self): return self memoized = moduleproperty(memoized, base=memoized_property) .. versionadded:: 1.6.1 Added the `base` parameter. """ import sys module = sys.modules[getter.__module__] module, _created, cls = customize(module) class prop(base): if getattr(base, "setter", False): def setter(self, func, _name=None): result = super().setter(func) setattr(cls, _name or func.__name__, result) return result if getattr(base, "deleter", False): def deleter(self, func, _name=None): result = super().deleter(func) setattr(cls, _name or func.__name__, result) return result result = prop(getter, doc=doc) name = getter.__name__ setattr(cls, getter.__name__, result) set_name = getattr(result, "__set_name__", None) if set_name is not None: set_name(cls, name) if setter: result = result.setter(setter, _name=name) if deleter: result = result.deleter(deleter, _name=name) return result
[docs]def get_module_path(module): """Gets the absolute path of a `module`. :param module: Either module object or a (dotted) string for the module. :returns: The path of the module. If the module is a package, returns the directory path (not the path to the ``__init__``). If `module` is a string and it's not absolute, raises a TypeError. """ from importlib import import_module from xotl.tools.fs.path import normalize_path mod = import_module(module) if isinstance(module, str) else module # The __path__ only exists for packages and does not include the # __init__.py path = mod.__path__[0] if hasattr(mod, "__path__") else mod.__file__ return normalize_path(path)