Skip to content

Commit 39d951a

Browse files
Merge pull request #138 from jscotka/guess_default_deco
guess proper return object type based on return value Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
2 parents 363c4cf + 026481c commit 39d951a

File tree

3 files changed

+187
-7
lines changed

3 files changed

+187
-7
lines changed

requre/helpers/guess_object.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright Contributors to the Packit project.
2+
# SPDX-License-Identifier: MIT
3+
4+
import yaml
5+
import logging
6+
import pickle
7+
import warnings
8+
9+
from typing import Any, Dict, Optional
10+
from requre.objects import ObjectStorage
11+
from requre.helpers.simple_object import Simple, Void
12+
13+
logger = logging.getLogger(__name__)
14+
GUESS_STR = "guess_type"
15+
16+
17+
class Guess(Simple):
18+
"""
19+
This class is able to store/read all simple types in requre
20+
Void, Simple, ObjectStorage (pickle)
21+
It select proper type if possible.
22+
23+
Warning: when it uses ObjectStorage, it may lead some secrets to pickled objects
24+
and it could be hidden inside object representation.
25+
"""
26+
27+
@staticmethod
28+
def guess_type(value):
29+
try:
30+
# Try to use type for storing simple output (list, dict, str, nums, etc...)
31+
yaml.safe_dump(value)
32+
return Simple
33+
except Exception:
34+
try:
35+
# Try to store anything serializable via pickle module
36+
pickle.dumps(value)
37+
return ObjectStorage
38+
except Exception:
39+
# do not store anything if not possible directly
40+
warnings.warn(
41+
"Guess class - nonserializable return object - "
42+
f"Using supressed output, are you sure? {value}"
43+
)
44+
return Void
45+
46+
def write(self, obj: Any, metadata: Optional[Dict] = None) -> Any:
47+
"""
48+
Write the object representation to storage
49+
Internally it will use self.to_serializable
50+
method to get serializable object representation
51+
52+
:param obj: some object
53+
:param metadata: store metedata to object
54+
:return: same obj
55+
"""
56+
object_serialization_type = self.guess_type(obj)
57+
metadata[GUESS_STR] = object_serialization_type.__name__
58+
instance = object_serialization_type(
59+
store_keys=self.store_keys,
60+
cassette=self.get_cassette(),
61+
storage_object_kwargs=self.storage_object_kwargs,
62+
)
63+
return instance.write(obj, metadata=metadata)
64+
65+
def read(self):
66+
"""
67+
Crete object representation of serialized data in persistent storage
68+
Internally it will use self.from_serializable method transform object
69+
70+
:return: proper object
71+
"""
72+
data = self.get_cassette()[self.store_keys]
73+
guess_type = self.get_cassette().data_miner.metadata[GUESS_STR]
74+
if guess_type == ObjectStorage.__name__:
75+
return pickle.loads(data)
76+
elif guess_type == Simple.__name__:
77+
return data
78+
elif guess_type == Void.__name__:
79+
return data
80+
else:
81+
raise ValueError(
82+
f"Unsupported type of stored object inside cassette: {guess_type}"
83+
)

requre/online_replacing.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
TEST_METHOD_REGEXP,
1616
)
1717
from requre.helpers.requests_response import RequestResponseHandling
18-
from requre.objects import ObjectStorage
1918
from requre.utils import get_datafile_filename
19+
from requre.helpers.guess_object import Guess
2020

2121
logger = logging.getLogger(__name__)
2222

@@ -287,9 +287,10 @@ def replace_module_match(
287287
:param storage_keys_strategy: you can change key strategy for storing data
288288
default simple one avoid to store stack information
289289
"""
290-
if (decorate is None and replace is None) or (
291-
decorate is not None and replace is not None
292-
):
290+
if decorate is None and replace is None:
291+
logger.info(f"Using default decorator for {what}")
292+
decorate = Guess.decorator_plain(cassette=cassette)
293+
elif decorate is not None and replace is not None:
293294
raise ValueError("right one from [decorate, replace] parameter has to be set.")
294295

295296
def decorator_cover(func):
@@ -357,9 +358,7 @@ def record(
357358
cassette.storage_file = storage_file
358359

359360
def _record_inner(func):
360-
return replace_module_match(
361-
what=what, cassette=cassette, decorate=ObjectStorage.decorator_all_keys
362-
)(func)
361+
return replace_module_match(what=what, cassette=cassette)(func)
363362

364363
return _record_inner
365364

@@ -443,6 +442,12 @@ def recording(
443442
cassette.data_miner.key_stategy_cls = storage_keys_strategy
444443
# ensure that directory structure exists already
445444
os.makedirs(os.path.dirname(cassette.storage_file), exist_ok=True)
445+
# use default decorator for context manager if not given.
446+
if decorate is None and replace is None:
447+
logger.info(f"Using default decorator for {what}")
448+
decorate = Guess.decorator_plain(cassette=cassette)
449+
elif decorate is not None and replace is not None:
450+
raise ValueError("right one from [decorate, replace] parameter has to be set.")
446451
# Store values and their replacements for modules to be able to revert changes back
447452
original_module_items = _parse_and_replace_sys_modules(
448453
what=what, cassette=cassette, decorate=decorate, replace=replace

tests/test_guess_objects.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import unittest
2+
import math
3+
import os
4+
from requre.objects import ObjectStorage
5+
from requre.utils import StorageMode
6+
from requre.helpers.guess_object import Guess, GUESS_STR
7+
from requre.helpers.simple_object import Void, Simple
8+
from requre.online_replacing import apply_decorator_to_all_methods, replace_module_match
9+
from requre.cassette import Cassette
10+
from tests.testbase import BaseClass
11+
from tests.test_object import OwnClass
12+
import sys
13+
14+
15+
class Unit(BaseClass):
16+
def testVoid(self):
17+
self.assertEqual(Void, Guess.guess_type(sys.__stdout__))
18+
19+
def testSimple(self):
20+
self.assertEqual(Simple, Guess.guess_type("abc"))
21+
self.assertEqual(Simple, Guess.guess_type({1: 2, "a": "b"}))
22+
self.assertNotEqual(ObjectStorage, Guess.guess_type({1: 2, "a": "b"}))
23+
24+
def testPickle(self):
25+
self.assertEqual(ObjectStorage, Guess.guess_type(OwnClass(1)))
26+
self.assertNotEqual(Void, Guess.guess_type(OwnClass(1)))
27+
self.assertNotEqual(Simple, Guess.guess_type(OwnClass(1)))
28+
29+
30+
def obj_return(num, obj):
31+
_ = num
32+
return obj
33+
34+
35+
class Store(BaseClass):
36+
def testFunctionDecorator(self):
37+
"""
38+
Check if it is able to guess and store/restore proper values with types
39+
"""
40+
decorated_own = Guess.decorator(cassette=self.cassette, item_list=[0])(
41+
obj_return
42+
)
43+
before1 = decorated_own(1, "abc")
44+
before2 = decorated_own(2, OwnClass(2))
45+
before3 = decorated_own(2, OwnClass(3))
46+
before4 = decorated_own(3, sys.__stdin__)
47+
self.cassette.dump()
48+
self.cassette.mode = StorageMode.read
49+
50+
after2 = decorated_own(2, OwnClass(2))
51+
self.assertEqual(
52+
self.cassette.data_miner.metadata[GUESS_STR], ObjectStorage.__name__
53+
)
54+
after3 = decorated_own(2, OwnClass(3))
55+
self.assertEqual(
56+
self.cassette.data_miner.metadata[GUESS_STR], ObjectStorage.__name__
57+
)
58+
after1 = decorated_own(1, "abc")
59+
self.assertEqual(self.cassette.data_miner.metadata[GUESS_STR], Simple.__name__)
60+
after4 = decorated_own(3, sys.__stdin__)
61+
self.assertEqual(self.cassette.data_miner.metadata[GUESS_STR], Void.__name__)
62+
63+
self.assertEqual(before1, after1)
64+
self.assertEqual(before2.__class__.__name__, after2.__class__.__name__)
65+
self.assertEqual(before3.__class__.__name__, after3.__class__.__name__)
66+
self.assertEqual(after2.__class__.__name__, "OwnClass")
67+
self.assertEqual(before4.__class__.__name__, "TextIOWrapper")
68+
self.assertEqual(after4.__class__.__name__, "str")
69+
70+
71+
@apply_decorator_to_all_methods(replace_module_match(what="math.sin"))
72+
class ApplyDefaultDecorator(unittest.TestCase):
73+
SIN_OUTPUT = 0.9974949866040544
74+
75+
def cassette_setup(self, cassette):
76+
self.assertEqual(cassette.storage_object, {})
77+
78+
def cassette_teardown(self, cassette):
79+
os.remove(cassette.storage_file)
80+
81+
def test(self, cassette: Cassette):
82+
math.sin(1.5)
83+
self.assertEqual(len(cassette.storage_object["math"]["sin"]), 1)
84+
self.assertAlmostEqual(
85+
self.SIN_OUTPUT,
86+
cassette.storage_object["math"]["sin"][0]["output"],
87+
delta=0.0005,
88+
)
89+
self.assertEqual(
90+
cassette.storage_object["math"]["sin"][0]["metadata"][GUESS_STR],
91+
"Simple",
92+
)

0 commit comments

Comments
 (0)