#!/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)