diff --git a/CHANGES.md b/CHANGES.md index 6a727d9..938d0ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,8 @@ # What's new? + + * Version 0.3.3 + - Swirl to a stabilized hook engine adopted by 0.3.0 + - Unhook is much safer than v0.3.2 * Version 0.3.2 - Improved hook engine. Now it can do safely unhook. diff --git a/ProtoText/__init__.py b/ProtoText/__init__.py index 842c388..f701eb8 100644 --- a/ProtoText/__init__.py +++ b/ProtoText/__init__.py @@ -1,8 +1,8 @@ from .models import MessageWrapper -from .hook_helper import register_class_hook, ClassHookHelper +from .hook_helper import register_class_hook, deregister_class_hook __author__ = 'zhengxu' -__version__ = '0.3.2' +__version__ = '0.3.3' try: from google.protobuf.message import Message @@ -15,19 +15,15 @@ # Hook Message class by MessageWrapper # MESSAGE_WRAPPER_CLASS = [MessageWrapper] -HOOK_HELPER_CLASS = [] def prototext_hook(): - global HOOK_HELPER_CLASS - HOOK_HELPER_CLASS = [ClassHookHelper(x) for x in MESSAGE_WRAPPER_CLASS] - for hhc in HOOK_HELPER_CLASS: - hhc.hook() + for mwc in MESSAGE_WRAPPER_CLASS: + register_class_hook(mwc) def prototext_unhook(): - for hhc in HOOK_HELPER_CLASS: - hhc.unhook() - + for mwc in MESSAGE_WRAPPER_CLASS: + deregister_class_hook(mwc) prototext_hook() diff --git a/ProtoText/hook_helper.py b/ProtoText/hook_helper.py index bb011db..7ef4cd1 100644 --- a/ProtoText/hook_helper.py +++ b/ProtoText/hook_helper.py @@ -1,83 +1,10 @@ import types -import inspect import __builtin__ import logging -logger = logging.getLogger(__name__) - -""" - TODO: - - Add ModuleHookHelper and remove the deprecated code - - Append the old function pointer to the original message object. - - Try to avoid multiple hook at the same time. -""" - - -class ClassHookHelper(object): - def __init__(self, cls, **kwargs): - """ - :param cls: the class object of the class hook - :param strategy: 'safe' or 'override' default: safe - :param skip_buildin: determine if we skip the build in object default: True - :return: - """ - # TODO: Add more strict check for eligible class - assert isinstance(cls, (type, types.ClassType)), \ - "The input object must be a class object" - self._hook_class = cls - self._hook_table = {} - self._strategy = kwargs.get('strategy', 'safe') - self._skip_buildin_class = kwargs.get('skip_buildin_class', True) - - def hook(self, strategy=None, skip_buildin=None): - strategy = strategy or self._strategy - skip_buildin = skip_buildin or self._skip_buildin_class - cls = self._hook_class - base_classes = cls.__bases__ - for base_class in base_classes: - if skip_buildin and base_class.__name__ in __builtin__.__dict__: - logger.warn("Skip hooking build-in base class '%s' in sub-class '%s' ..." % - (base_class.__name__, cls.__name__)) - continue - for x in cls.__dict__: - if not (x in base_class.__dict__ and strategy == 'safe'): - # instance method - if isinstance(cls.__dict__[x], (types.FunctionType, classmethod, staticmethod, property)): - # build hook table - if not (base_class.__name__ in self._hook_table and - isinstance(self._hook_table[base_class.__name__], dict)): - self._hook_table[base_class.__name__] = {} - self._hook_table[base_class.__name__][x] = \ - (base_class.__dict__[x] if base_class.__dict__.has_key(x) else None, cls.__dict__[x]) - logger.debug("Wrapping [%s] %s" % (x, str(cls.__dict__[x]))) - setattr(base_class, x, cls.__dict__[x]) - else: - logger.debug("Skip wrapping [%s] %s for security consideration" % (x, str(cls.__dict__[x]))) +HOOK_TABLE_NAME = '__ZX_PY_HOOK_TABLE__' - def unhook(self): - cls = self._hook_class - base_classes = cls.__bases__ - for base_class in base_classes: - if base_class.__name__ in self._hook_table: - _sub_hook_table = self._hook_table[base_class.__name__] - for x in cls.__dict__: - if x in _sub_hook_table: - if _sub_hook_table[x][1] != cls.__dict__[x]: - logger.error("Error! This hook is not installed by this helper. [%s] %s != %s " % - (x, _sub_hook_table[x][1], str(cls.__dict__[x]))) - continue - if _sub_hook_table[x][0] is None: - logger.debug("Delete [%s] %s for unhooking" % (x, str(cls.__dict__[x]))) - delattr(base_class, x) - else: - logger.debug("Recover [%s] %s to %s for unhooking" % - (x, str(cls.__dict__[x]), str(_sub_hook_table[x][0]))) - setattr(base_class, x, _sub_hook_table[x][0]) - - -""" - Deprecated Code, subject to elimination in the near future. -""" +logger = logging.getLogger(__name__) def register_class_hook(cls, strategy='safe', skip_buildin=True): @@ -95,11 +22,23 @@ def register_class_hook(cls, strategy='safe', skip_buildin=True): logger.warn("Skip hooking build-in base class '%s' in sub-class '%s' ..." % (base_class.__name__, cls.__name__)) continue + if HOOK_TABLE_NAME in base_class.__dict__: + logger.warn("Skip hooking the base class %s to avoid multi-hook conflict ..." % + base_class.__name__) + continue + else: + # build HOOK_TABLE + setattr(base_class, HOOK_TABLE_NAME, {}) + assert HOOK_TABLE_NAME in base_class.__dict__, "Unable to append hook table to target class" for x in cls.__dict__: if not (x in base_class.__dict__ and strategy == 'safe'): # instance method if isinstance(cls.__dict__[x], (types.FunctionType, classmethod, staticmethod, property)): logger.debug("Wrapping [%s] %s" % (x, str(cls.__dict__[x]))) + # Append hook table + base_class.__dict__[HOOK_TABLE_NAME][x] = base_class.__dict__[x] \ + if x in base_class.__dict__ else None + # Set hook setattr(base_class, x, cls.__dict__[x]) else: logger.debug("Skip wrapping [%s] %s for security consideration" % (x, str(cls.__dict__[x]))) @@ -114,43 +53,14 @@ def deregister_class_hook(cls): "The input object must be a class object" base_classes = cls.__bases__ for base_class in base_classes: - for x in cls.__dict__: - if x in base_class.__dict__: - # instance method - if cls.__dict__[x] == base_class.__dict__[x] and \ - isinstance(cls.__dict__[x], (types.FunctionType, classmethod, staticmethod, property)): - logger.debug("Remove warping [%s] %s" % (x, str(cls.__dict__[x]))) - delattr(base_class, x) - - -def register_module_hook(class_name_list=[], allow_recursive=False, **kwargs): - """ - Enumerate all classes in the caller module, hook all or selected classes' base - classes with specific module - :param class_name_list: given the class list you'd like to hook - :return: - """ - try: - # - # Fetch the caller module - # - parent_frame = inspect.stack()[1][0] - caller_module = inspect.getmodule(parent_frame) - class_list = [v for k, v in caller_module.__dict__.iteritems() - if ((k in class_name_list) or not class_name_list) and - isinstance(v, (type, types.ClassType))] - base_classes_list = reduce(lambda x, y: x + list(y.__bases__), class_list, []) - if not allow_recursive: - class_list = filter(lambda x: x not in base_classes_list, class_list) - except Exception, e: - logger.error("Unable to retrieve the caller module : %s" % str(e)) - return - for c in class_list: - try: - # - # Do the class hook - # - register_class_hook(c, **kwargs) - except Exception, e: - logger.error("Unable to register class hook") + if HOOK_TABLE_NAME not in base_class.__dict__: + logger.warn("Unable to find Hook Table for class '%s', " + "probably it's not properly hooked." % base_class.__name__) continue + _hook_table = base_class.__dict__[HOOK_TABLE_NAME] + for x in _hook_table: + if _hook_table[x]: + setattr(base_class, x, _hook_table[x]) + else: + delattr(base_class, x) + delattr(base_class, HOOK_TABLE_NAME) \ No newline at end of file diff --git a/README.md b/README.md index f44a25c..dfe02d4 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ first.** ### Installation -The newest release version is `0.3.0` even though we have some unstable development version ahead of that. +The newest release version is `0.3.3` albeit we have some unstable development version ahead of that. To install the package, simply use the `pip` manager: ```bash -pip install https://github.com/XericZephyr/prototext/archive/v0.3.0.tar.gz +pip install https://github.com/XericZephyr/prototext/archive/v0.3.3.tar.gz ``` We will publish this module to PyPI as soon as we consider this module as stable. @@ -38,11 +38,11 @@ You don't need to anything after that. The hack will be completed automatically If you want to do safely removing the prototext hook (pls don't, pls), use ```python -ProtoText.unhook() +ProtoText.prototext_unhook() ``` ~~The unhook part is still very buggy. We are sorry for that.~~ -(You didn't see anything in the line above.) +(We've fixed these bugs. It's safe now. :love_letter: ) #### Dict-Like Operations