Skip to content

Commit

Permalink
now scanning and mocking classes instead of using MockObj
Browse files Browse the repository at this point in the history
  • Loading branch information
w4rum committed Jan 4, 2019
1 parent db7ba88 commit 1282c99
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 208 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.pickle
*.state
**/__pycache__
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Python MockPickler

Python Pickle variant that wraps unknown classes in a mock class.
These classes can be pickled again and should load correctly on the system in
Python `pickle` variant that mocks unknown modules and classes.
These classes can be pickled again and should load correctly on the system on
which the pickle was originally created.

## Usage (Unpickler)
Expand All @@ -13,7 +13,7 @@ import mockpickle as pickle
pickled_data = load_from_somewhere()

mocked_data = pickle.loads(pickled_data)
mocked_data._state["foo"] = "not_foo"
mocked_data.foo = "not_foo"
repickled_data = pickle.dumps(mocked_data)
```

Expand All @@ -22,15 +22,25 @@ Just run `python3 mockpickler-gui` and try not to choke on the bugs.

Oh, and don't look at the code if you value your eyesight.

## Stability
This is a rather hacky and experimental script.
The repickling relies on ugly eval statements that dynamically load the unknown
modules on the target system.
Because of this, you cannot mockunpickle data that has been dumped with
mockpickler, so `loads(dumps(x))` will fail.
This is because the mockunpickler currently can not handle the `__reduce__`
outputs from `MockObj`.

`list` and `dict` subclasses are treated differently because their contents
are not part of the object attributes (`__dict__`).
Repickling might fail for subclasses of other built-in types.
## How it works
Before actually unpickling the data, the `load` functions make a scanning run
in which they dynamically create missing modules and classes.
The rest of the `load` and `dump` functions are the same as in the standard
`pickle` module.

## Limitations
Since pickles only contain module and class names but not any information
regarding superclasses, the scanner tries to guess superclasses based on the
operations that are performed on the mocked classes, e.g. assume `list` as
superclass when `load_appends` is executed on an object of the class.

Currently, only `list` and `dict` subclasses are detected this way.
Subclasses of other built-in types, e.g. `set`, might not unpickle properly.

Since the unpickler does not have any information about an unknown class's
`__getstate__` and `__getnewargs__` functions, the arguments passed to
`__new__` upon unpickling are preserved so that they're not lost when
repickling the data.
This is also done for arguments passed to `__setstate__` if the argument is
not a `dict`.
Otherwise, it's simply copied onto `__dict__`.
6 changes: 3 additions & 3 deletions demo/demo-read.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
mocked_data = pickle.loads(pickled_data)

print("--- Mocked unpickle:")
print(mocked_data)
print(mocked_data.__dict__)

print("--- Manipulating foo in mock object and repickling...")
mocked_data._state['foo'] = "not_foo"
print(mocked_data)
mocked_data.foo = "not_foo"
print(mocked_data.__dict__)
repickled_data = pickle.dumps(mocked_data)

with open(filenameOut, "wb") as f:
Expand Down
2 changes: 2 additions & 0 deletions demo/run.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/bin/sh

rm demo.pickle demo-repickled.pickle

python3 target-system/demo-write.py
python3 demo-read.py
python3 target-system/demo-read-target.py
18 changes: 1 addition & 17 deletions mockpickle-gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ def addItem(self, obj, parent='', name='', setter=None):
if not isinstance(obj, primitive):
# non-primitive types are searched recursively
typename = t.__name__
if issubclass(t, mockpickle.MockObj):
typename = "%s (%s)" % (obj._cls, obj._supercls.__name__)
me = self.tree.insert(parent, 'end', text=name,
values=(typename, ''))
self.backbinds[me] = obj
Expand Down Expand Up @@ -145,20 +143,6 @@ def expand(self, item):
it = v()
break

# Unmock items
if issubclass(t, mockpickle.MockObj):
its = {
dict: lambda: obj._pickledict.items(),
list: lambda: enumerate(obj._picklelist),
}
for k, v in its.items():
if issubclass(t, k):
it = v()
break
if it is None:
# General mocked objects
it = obj._state.items()

if it is None:
# Fallback for general objects
it = obj.__dict__.items()
Expand All @@ -173,7 +157,7 @@ def expand(self, item):
def updateItem(self, item, value):
owner, key = self.setters[item]
t = type(owner[key])
print("%s: %s => %s" % (item, owner[key], value))
# print("%s: %s => %s" % (item, owner[key], value))
# Fail when types don't match
try:
value = t(value)
Expand Down
Loading

0 comments on commit 1282c99

Please sign in to comment.