Skip to content

Commit ba36d08

Browse files
committed
Push the version to 0.3.3. Stabilize the hook engine. Now the unhook functionality is safe and stable
1 parent 0f3a84f commit ba36d08

File tree

4 files changed

+38
-128
lines changed

4 files changed

+38
-128
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# What's new?
2+
3+
* Version 0.3.3
4+
- Swirl to a stabilized hook engine adopted by 0.3.0
5+
- Unhook is much safer than v0.3.2
26

37
* Version 0.3.2
48
- Improved hook engine. Now it can do safely unhook.

ProtoText/__init__.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from .models import MessageWrapper
2-
from .hook_helper import register_class_hook, ClassHookHelper
2+
from .hook_helper import register_class_hook, deregister_class_hook
33

44
__author__ = 'zhengxu'
5-
__version__ = '0.3.2'
5+
__version__ = '0.3.3'
66

77
try:
88
from google.protobuf.message import Message
@@ -15,19 +15,15 @@
1515
# Hook Message class by MessageWrapper
1616
#
1717
MESSAGE_WRAPPER_CLASS = [MessageWrapper]
18-
HOOK_HELPER_CLASS = []
1918

2019

2120
def prototext_hook():
22-
global HOOK_HELPER_CLASS
23-
HOOK_HELPER_CLASS = [ClassHookHelper(x) for x in MESSAGE_WRAPPER_CLASS]
24-
for hhc in HOOK_HELPER_CLASS:
25-
hhc.hook()
21+
for mwc in MESSAGE_WRAPPER_CLASS:
22+
register_class_hook(mwc)
2623

2724

2825
def prototext_unhook():
29-
for hhc in HOOK_HELPER_CLASS:
30-
hhc.unhook()
31-
26+
for mwc in MESSAGE_WRAPPER_CLASS:
27+
deregister_class_hook(mwc)
3228

3329
prototext_hook()

ProtoText/hook_helper.py

Lines changed: 24 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,10 @@
11
import types
2-
import inspect
32
import __builtin__
43
import logging
54

6-
logger = logging.getLogger(__name__)
7-
8-
"""
9-
TODO:
10-
- Add ModuleHookHelper and remove the deprecated code
11-
- Append the old function pointer to the original message object.
12-
- Try to avoid multiple hook at the same time.
13-
"""
14-
15-
16-
class ClassHookHelper(object):
17-
def __init__(self, cls, **kwargs):
18-
"""
19-
:param cls: the class object of the class hook
20-
:param strategy: 'safe' or 'override' default: safe
21-
:param skip_buildin: determine if we skip the build in object default: True
22-
:return:
23-
"""
24-
# TODO: Add more strict check for eligible class
25-
assert isinstance(cls, (type, types.ClassType)), \
26-
"The input object must be a class object"
27-
self._hook_class = cls
28-
self._hook_table = {}
29-
self._strategy = kwargs.get('strategy', 'safe')
30-
self._skip_buildin_class = kwargs.get('skip_buildin_class', True)
31-
32-
def hook(self, strategy=None, skip_buildin=None):
33-
strategy = strategy or self._strategy
34-
skip_buildin = skip_buildin or self._skip_buildin_class
35-
cls = self._hook_class
36-
base_classes = cls.__bases__
37-
for base_class in base_classes:
38-
if skip_buildin and base_class.__name__ in __builtin__.__dict__:
39-
logger.warn("Skip hooking build-in base class '%s' in sub-class '%s' ..." %
40-
(base_class.__name__, cls.__name__))
41-
continue
42-
for x in cls.__dict__:
43-
if not (x in base_class.__dict__ and strategy == 'safe'):
44-
# instance method
45-
if isinstance(cls.__dict__[x], (types.FunctionType, classmethod, staticmethod, property)):
46-
# build hook table
47-
if not (base_class.__name__ in self._hook_table and
48-
isinstance(self._hook_table[base_class.__name__], dict)):
49-
self._hook_table[base_class.__name__] = {}
50-
self._hook_table[base_class.__name__][x] = \
51-
(base_class.__dict__[x] if base_class.__dict__.has_key(x) else None, cls.__dict__[x])
52-
logger.debug("Wrapping [%s] %s" % (x, str(cls.__dict__[x])))
53-
setattr(base_class, x, cls.__dict__[x])
54-
else:
55-
logger.debug("Skip wrapping [%s] %s for security consideration" % (x, str(cls.__dict__[x])))
5+
HOOK_TABLE_NAME = '__ZX_PY_HOOK_TABLE__'
566

57-
def unhook(self):
58-
cls = self._hook_class
59-
base_classes = cls.__bases__
60-
for base_class in base_classes:
61-
if base_class.__name__ in self._hook_table:
62-
_sub_hook_table = self._hook_table[base_class.__name__]
63-
for x in cls.__dict__:
64-
if x in _sub_hook_table:
65-
if _sub_hook_table[x][1] != cls.__dict__[x]:
66-
logger.error("Error! This hook is not installed by this helper. [%s] %s != %s " %
67-
(x, _sub_hook_table[x][1], str(cls.__dict__[x])))
68-
continue
69-
if _sub_hook_table[x][0] is None:
70-
logger.debug("Delete [%s] %s for unhooking" % (x, str(cls.__dict__[x])))
71-
delattr(base_class, x)
72-
else:
73-
logger.debug("Recover [%s] %s to %s for unhooking" %
74-
(x, str(cls.__dict__[x]), str(_sub_hook_table[x][0])))
75-
setattr(base_class, x, _sub_hook_table[x][0])
76-
77-
78-
"""
79-
Deprecated Code, subject to elimination in the near future.
80-
"""
7+
logger = logging.getLogger(__name__)
818

829

8310
def register_class_hook(cls, strategy='safe', skip_buildin=True):
@@ -95,11 +22,23 @@ def register_class_hook(cls, strategy='safe', skip_buildin=True):
9522
logger.warn("Skip hooking build-in base class '%s' in sub-class '%s' ..." %
9623
(base_class.__name__, cls.__name__))
9724
continue
25+
if HOOK_TABLE_NAME in base_class.__dict__:
26+
logger.warn("Skip hooking the base class %s to avoid multi-hook conflict ..." %
27+
base_class.__name__)
28+
continue
29+
else:
30+
# build HOOK_TABLE
31+
setattr(base_class, HOOK_TABLE_NAME, {})
32+
assert HOOK_TABLE_NAME in base_class.__dict__, "Unable to append hook table to target class"
9833
for x in cls.__dict__:
9934
if not (x in base_class.__dict__ and strategy == 'safe'):
10035
# instance method
10136
if isinstance(cls.__dict__[x], (types.FunctionType, classmethod, staticmethod, property)):
10237
logger.debug("Wrapping [%s] %s" % (x, str(cls.__dict__[x])))
38+
# Append hook table
39+
base_class.__dict__[HOOK_TABLE_NAME][x] = base_class.__dict__[x] \
40+
if x in base_class.__dict__ else None
41+
# Set hook
10342
setattr(base_class, x, cls.__dict__[x])
10443
else:
10544
logger.debug("Skip wrapping [%s] %s for security consideration" % (x, str(cls.__dict__[x])))
@@ -114,43 +53,14 @@ def deregister_class_hook(cls):
11453
"The input object must be a class object"
11554
base_classes = cls.__bases__
11655
for base_class in base_classes:
117-
for x in cls.__dict__:
118-
if x in base_class.__dict__:
119-
# instance method
120-
if cls.__dict__[x] == base_class.__dict__[x] and \
121-
isinstance(cls.__dict__[x], (types.FunctionType, classmethod, staticmethod, property)):
122-
logger.debug("Remove warping [%s] %s" % (x, str(cls.__dict__[x])))
123-
delattr(base_class, x)
124-
125-
126-
def register_module_hook(class_name_list=[], allow_recursive=False, **kwargs):
127-
"""
128-
Enumerate all classes in the caller module, hook all or selected classes' base
129-
classes with specific module
130-
:param class_name_list: given the class list you'd like to hook
131-
:return:
132-
"""
133-
try:
134-
#
135-
# Fetch the caller module
136-
#
137-
parent_frame = inspect.stack()[1][0]
138-
caller_module = inspect.getmodule(parent_frame)
139-
class_list = [v for k, v in caller_module.__dict__.iteritems()
140-
if ((k in class_name_list) or not class_name_list) and
141-
isinstance(v, (type, types.ClassType))]
142-
base_classes_list = reduce(lambda x, y: x + list(y.__bases__), class_list, [])
143-
if not allow_recursive:
144-
class_list = filter(lambda x: x not in base_classes_list, class_list)
145-
except Exception, e:
146-
logger.error("Unable to retrieve the caller module : %s" % str(e))
147-
return
148-
for c in class_list:
149-
try:
150-
#
151-
# Do the class hook
152-
#
153-
register_class_hook(c, **kwargs)
154-
except Exception, e:
155-
logger.error("Unable to register class hook")
56+
if HOOK_TABLE_NAME not in base_class.__dict__:
57+
logger.warn("Unable to find Hook Table for class '%s', "
58+
"probably it's not properly hooked." % base_class.__name__)
15659
continue
60+
_hook_table = base_class.__dict__[HOOK_TABLE_NAME]
61+
for x in _hook_table:
62+
if _hook_table[x]:
63+
setattr(base_class, x, _hook_table[x])
64+
else:
65+
delattr(base_class, x)
66+
delattr(base_class, HOOK_TABLE_NAME)

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ first.**
1212

1313
### Installation
1414

15-
The newest release version is `0.3.0` even though we have some unstable development version ahead of that.
15+
The newest release version is `0.3.3` albeit we have some unstable development version ahead of that.
1616
To install the package, simply use the `pip` manager:
1717

1818
```bash
19-
pip install https://github.com/XericZephyr/prototext/archive/v0.3.0.tar.gz
19+
pip install https://github.com/XericZephyr/prototext/archive/v0.3.3.tar.gz
2020
```
2121

2222
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
3838
If you want to do safely removing the prototext hook (pls don't, pls), use
3939

4040
```python
41-
ProtoText.unhook()
41+
ProtoText.prototext_unhook()
4242
```
4343

4444
~~The unhook part is still very buggy. We are sorry for that.~~
45-
(You didn't see anything in the line above.)
45+
(We've fixed these bugs. It's safe now. :love_letter: )
4646

4747
#### Dict-Like Operations
4848

0 commit comments

Comments
 (0)