Source code for compat_patcher_core.utilities

from __future__ import absolute_import, print_function, unicode_literals

import functools
import importlib
import logging
import sys
import types
import warnings as stdlib_warnings  # Do NOT import/use elsewhere than here!

import six


[docs]def tuplify_software_version(version): """ Coerces the version string (if not None), to a version tuple. E.g. "1.7.0" becomes (1, 7, 0). No version suffix like "alpha" is expected. """ if version is None: return version if isinstance(version, six.string_types): version = tuple(int(x) for x in version.split(".")) assert len(version) <= 5, version assert all(isinstance(x, six.integer_types) for x in version), version return version
[docs]def detuplify_software_version(version): """ Coerces the version tuple (if not None), to a version string. E.g. (1, 7, 0) becomes "1.7.0". """ if version is None: return version if isinstance(version, (tuple, list)): version = ".".join(str(number) for number in version) assert isinstance(version, six.string_types) return version
[docs]class WarningsProxy(object): """An instance of this class acts as a replacement for the stdlib "warnings" package, but it relies on a PatchingUtilities instance as soon as this one is provided - thus making Warnings controllable by compat patcher settings. """ _patching_utilities = None def set_patching_utilities(self, patching_utilities): assert isinstance(patching_utilities, PatchingUtilities), patching_utilities self._patching_utilities = patching_utilities def warn(self, *args, **kwargs): if self._patching_utilities: self._patching_utilities.emit_warning(*args, **kwargs) else: # Compat patcher is not yet configured stdlib_warnings.warn(*args, **kwargs)
[docs]class PatchingUtilities(object): """ An instance of this class is provided as first argument to each compatibility fixer applied. It provides handy tools to monkey-patch the software environment, and emit logs and Warnings in a controllable way. For better forward-compatibility, please call injection utilities though keyword arguments, and not positional ones (due to python2.7 support, it's not enforced yet). """ _logging_level = None _enable_warnings = False _patch_injected_objects = None settings_keys_used = ["logging_level", "enable_warnings", "patch_injected_objects"] def __init__(self, settings): # We force extraction of values, in case settings is a lazy instance # and not just a dict assert settings, settings settings = {name: settings[name] for name in self.settings_keys_used} self.apply_settings(settings)
[docs] def apply_settings(self, settings): """This method can be called at runtime, mainly to alter the emission of logs and Warnings by fixers. it's possible to provide only a subset of settings, the others remaining as is. """ if "logging_level" in settings: assert settings["logging_level"] is None or hasattr( logging, settings["logging_level"] ), settings["logging_level"] self._logging_level = settings["logging_level"] if "enable_warnings" in settings: assert settings["enable_warnings"] in (True, False), settings[ "enable_warnings" ] self._enable_warnings = settings["enable_warnings"] if "patch_injected_objects" in settings: patch_injected_objects = settings["patch_injected_objects"] if patch_injected_objects is True: patch_injected_objects = "__COMPAT_PATCHED__" # Default marker name assert not patch_injected_objects or isinstance( patch_injected_objects, six.string_types ), repr(patch_injected_objects) self._patch_injected_objects = patch_injected_objects
@staticmethod def _is_simple_callable(obj): return isinstance( obj, (types.FunctionType, types.BuiltinFunctionType, functools.partial) ) def _patch_injected_object(self, object_to_patch): """Mark shim object with a custom boolean attribute, to help identify it via introspection. Returns a boolean indicating whether the marking worked (some objects don't have a writable __dict__). """ assert object_to_patch not in (True, False, None), object_to_patch if self._patch_injected_objects: try: setattr(object_to_patch, self._patch_injected_objects, True) return True except AttributeError: return False # properties, bound methods and such can't be modified return None
[docs] def emit_log(self, message, level="INFO"): """A logger printing to stderr, since at some stages of patching, logging is not yet setup. Log is only output if `level` is gerater or equal the current `logging_level` setting. """ min_logging_level = self._logging_level if min_logging_level is None: return # No logging at all if getattr(logging, level) < getattr(logging, min_logging_level): return full_message = "[DCP_%s] %s" % (level, message) print(full_message, file=sys.stderr)
[docs] def emit_warning(self, message, category=DeprecationWarning, stacklevel=1): """Similar to "warnings.warn()" of the stdlib, but only emits the Warning if `enable_warnings` setting is True. """ if self._enable_warnings: stdlib_warnings.warn(message, category, stacklevel + 1)
[docs] def inject_attribute(self, target_object, target_attrname, attribute): """Inject an attribute into an object of any type (module, class, instance...). :param target_object: The object to patch :param target_attrname: The name given to the new attribute in the object to patch :param attribute: The attribute to inject, which must not be a callable """ assert attribute is not None assert not self._is_simple_callable(attribute), attribute assert not isinstance(attribute, six.class_types), attribute self._patch_injected_object(attribute) setattr(target_object, target_attrname, attribute)
[docs] def inject_callable(self, target_object, target_callable_name, patch_callable): """Inject a simple callable (not a class) into an object of any type (module, class, instance...). :param target_object: The object to patch :param target_callable_name: The name given to the new callable in the object to patch :param patch_callable: The callable to inject, which must be a callable, but not a class """ assert self._is_simple_callable(patch_callable), patch_callable self._patch_injected_object(patch_callable) setattr(target_object, target_callable_name, patch_callable)
[docs] def inject_callable_alias( self, target_object, target_attrname, source_object, source_attrname ): """ Create and inject an alias for the source callable (not a class), which also triggers a deprecation warning when called. Returns the created alias callable. :param target_object: The object to patch :param target_attrname: The name of the callable on the target object :param source_object: The object from which to get the callable :param source_attrname: The name of the callable on the source object """ source_callable = getattr(source_object, source_attrname) assert self._is_simple_callable(source_callable), source_callable @functools.wraps(source_callable) def wrapper(*args, **kwds): # we dunno if it's a backwards or forwards compatibility shim... self.emit_warning( "%s.%s, which is an alias for %s.%s, was called. One of these is deprecated." % (target_object, source_attrname, source_object, source_attrname), category=DeprecationWarning, ) return source_callable(*args, **kwds) self._patch_injected_object(wrapper) setattr(target_object, target_attrname, wrapper) return wrapper
[docs] def inject_class(self, target_object, target_klassname, klass): """Inject a class into an object of any type (module, class, instance...). :param target_object: The object to patch :param target_klassname: The name given to the new class in the object to patch :param klass: The class to inject """ assert isinstance(klass, six.class_types), klass self._patch_injected_object(klass) setattr(target_object, target_klassname, klass)
[docs] def inject_module(self, target_module_name, module): """Inject a module in sys.modules, under the selected dotted name. When injecting a submodule, its parent module(s) must exist too. It is good practice to, then, also inject this module object as an attribute of its immediate parents (with inject_attribute()), since this is normally done during python imports. :param target_module_name: The dotted name of the new module in sys.modules :param module: The module object to inject """ target_module_name = str(target_module_name) # Python2 compatibility assert isinstance(module, types.ModuleType), module assert sys.modules.get(target_module_name) is None, target_module_name self._patch_injected_object(module) sys.modules[target_module_name] = module
[docs] def inject_import_alias(self, alias_name, real_name): """Create an import alias for the selected module. This doesn't directly patch sys.modules, but instead uses the imports hooks of python, so that "import <alias_name>" and "import <real_name>" both end up importing the same the (sub)module object. When the alias is a submodule, its parent module(s) must exist too. :param alias_name: The dotted name of the alias module :param real_name: The dotted name of the real module """ from compat_patcher_core import import_proxifier import_proxifier.install_import_proxifier() # idempotent activation import_proxifier.register_module_alias( alias_name=alias_name, real_name=real_name )
def _import_attribute_from_dotted_string(dotted_string): """Turns `mymodule.mysubmodule.my_attr` into the imported my_attr object, be it a class or an instance. """ module_name, attr_name = dotted_string.rsplit(".", 1) module = importlib.import_module(module_name) attribute = getattr(module, attr_name) return attribute