Skip to content

Commit

Permalink
Push the version to 0.3.3. Stabilize the hook engine. Now the unhook …
Browse files Browse the repository at this point in the history
…functionality is safe and stable
  • Loading branch information
XericZephyr committed Oct 7, 2015
1 parent 0f3a84f commit ba36d08
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 128 deletions.
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
16 changes: 6 additions & 10 deletions ProtoText/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
138 changes: 24 additions & 114 deletions ProtoText/hook_helper.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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])))
Expand All @@ -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)
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down

0 comments on commit ba36d08

Please sign in to comment.