├── test ├── base_test │ ├── __init__.py │ ├── context_test │ │ ├── __init__.py │ │ ├── ctx_test.py │ │ └── context_test.py │ ├── function_test │ │ ├── __init__.py │ │ ├── partial_test.py │ │ ├── wrapper_function_test.py │ │ ├── context_function_test.py │ │ ├── method_test.py │ │ └── function_test.py │ ├── expression_test.py │ ├── thread_local_test.py │ ├── proxy_test.py │ └── template_test.py ├── util_test │ ├── __init__.py │ └── modutil_test.py ├── decorators_test │ ├── __init__.py │ ├── validations_test │ │ ├── __init__.py │ │ ├── engine_test.py │ │ └── validators_test.py │ ├── probability_test.py │ ├── remove_extra_args_test.py │ ├── profile_test.py │ ├── retries_test.py │ ├── once_test.py │ ├── timeout_test.py │ ├── locks_test.py │ ├── timeit_test.py │ ├── files_test.py │ ├── conditional_test.py │ ├── cache_test.py │ └── events_test.py ├── testutil.py └── testing_test.py ├── src └── decorated │ ├── base │ ├── __init__.py │ ├── false.py │ ├── thread_local.py │ ├── proxy.py │ ├── expression.py │ ├── template.py │ ├── dict.py │ ├── context.py │ └── function.py │ ├── util │ ├── __init__.py │ ├── reporters.py │ ├── gcutil.py │ ├── modutil.py │ └── dutil.py │ ├── decorators │ ├── __init__.py │ ├── validations │ │ ├── validators │ │ │ ├── __init__.py │ │ │ ├── string.py │ │ │ ├── base.py │ │ │ ├── misc.py │ │ │ └── number.py │ │ ├── errors.py │ │ ├── engine.py │ │ └── __init__.py │ ├── remove_extra_args.py │ ├── probability.py │ ├── once.py │ ├── conditional.py │ ├── retries.py │ ├── profile.py │ ├── timeout.py │ ├── locks.py │ ├── files.py │ ├── timeit.py │ ├── cache.py │ └── events.py │ ├── testing.py │ └── __init__.py ├── .gitignore ├── docs ├── decorators │ ├── profile.md │ ├── remove_extra_args.md │ ├── retries.md │ ├── conditional.md │ ├── timeout.md │ ├── timeit.md │ ├── synchronized.md │ ├── events.md │ ├── once.md │ └── cache.md ├── loggingd.md ├── advanced │ ├── chaining.md │ ├── wrapper.md │ ├── method.md │ └── arguments.md └── basic.md ├── setup.py └── README.md /test/base_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/util_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/decorated/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/decorated/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/decorators_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/decorated/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/base_test/context_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/base_test/function_test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/decorators_test/validations_test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /src/decorated/decorators/validations/validators/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /src/decorated/decorators/validations/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class ValidationError(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /src/decorated/util/reporters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import six 4 | 5 | PRINT_REPORTER = six.print_ 6 | LOG_REPORTER = logging.getLogger(__name__).debug 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # common 2 | .* 3 | *~ 4 | *.swp 5 | 6 | # python 7 | *.pyc 8 | __pycache__ 9 | 10 | # pip 11 | build 12 | dist 13 | MANIFEST 14 | src/decorated.egg-info 15 | 16 | *.html 17 | 18 | -------------------------------------------------------------------------------- /src/decorated/testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from fixtures2 import PatchesFixture 3 | 4 | 5 | class DecoratedFixture(PatchesFixture): 6 | def disable(self, cls): 7 | cls.enabled = False 8 | 9 | def enable(self, cls): 10 | cls.enabled = True 11 | -------------------------------------------------------------------------------- /src/decorated/decorators/remove_extra_args.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.function import Function 3 | 4 | class RemoveExtraArgs(Function): 5 | def _call(self, *args, **kw): 6 | kw = self._resolve_args(*args, **kw) 7 | return super(RemoveExtraArgs, self)._call(**kw) 8 | -------------------------------------------------------------------------------- /src/decorated/base/false.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class FalseObject(object): 4 | ''' 5 | >>> bool(FalseObject()) 6 | False 7 | ''' 8 | 9 | def __bool__(self): 10 | return False 11 | 12 | def __nonzero__(self): 13 | return False 14 | 15 | NOTSET = FalseObject() 16 | -------------------------------------------------------------------------------- /test/util_test/modutil_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.util import modutil 3 | from unittest.case import TestCase 4 | 5 | class LoadTreeTest(TestCase): 6 | def test_package(self): 7 | modutil.load_tree('decorated') 8 | 9 | def test_module(self): 10 | modutil.load_tree('decorated.util.modutil') 11 | -------------------------------------------------------------------------------- /src/decorated/util/gcutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import gc 3 | 4 | class DisableGc(object): 5 | def __init__(self): 6 | self._enabled = None 7 | 8 | def __enter__(self): 9 | self._enabled = gc.isenabled() 10 | gc.disable() 11 | 12 | def __exit__(self, *args, **kw): 13 | if self._enabled: 14 | gc.enable() 15 | -------------------------------------------------------------------------------- /docs/decorators/profile.md: -------------------------------------------------------------------------------- 1 | # Profile 2 | 3 | from decorated import profile 4 | 5 | @profile(iterations=100) 6 | def heavy_calc(): 7 | pass 8 | 9 | heavy_calc() 10 | 11 | This profiles the function using cProfile. 12 | The result is written to stdout. 13 | You can also use the LOGGING_REPORTER to write the result to logging. 14 | Refer to [timeit](timeit.md) for more information. 15 | -------------------------------------------------------------------------------- /test/decorators_test/probability_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.decorators.probability import Probability 3 | from testutil import DecoratedTest 4 | 5 | 6 | class ProbabilityTest(DecoratedTest): 7 | def test(self): 8 | @Probability(0.5) 9 | def add(a, b): 10 | return a + b 11 | result = add(1, 2) 12 | self.assertIn(result, [3, None]) 13 | -------------------------------------------------------------------------------- /docs/decorators/remove_extra_args.md: -------------------------------------------------------------------------------- 1 | # Remove\_extra\_args 2 | 3 | from decorated import remove_extra_args 4 | 5 | @remove_extra_args 6 | def handle(id): 7 | pass 8 | 9 | handle(123) 10 | handle(id=123) 11 | handle(id=123, timestamp=123456789) # the timestamp argument will be ignored 12 | 13 | This is convenient when handling GET/POST requests which are usually messed up with additional fields. 14 | -------------------------------------------------------------------------------- /docs/loggingd.md: -------------------------------------------------------------------------------- 1 | # Loggingd 2 | 3 | Loggingd is a logging framework based on decorated. 4 | 5 | from loggingd import log_enter, log_return, log_error 6 | 7 | @log_enter('[DEBUG] Calculating {a} / {b} ...') 8 | @log_return('Result is {ret}.') 9 | @log_error('[WARN] Failed to calc.', exc_info=True) 10 | def divide(a, b): 11 | return a / b 12 | -------------------------------------------------------------------------------- /src/decorated/decorators/probability.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | 4 | from decorated.base.function import Function 5 | 6 | 7 | class Probability(Function): 8 | def _call(self, *args, **kw): 9 | if random.random() < self._p: 10 | return super(Probability, self)._call(*args, **kw) 11 | else: 12 | return None 13 | 14 | def _init(self, p): 15 | self._p = p 16 | -------------------------------------------------------------------------------- /docs/decorators/retries.md: -------------------------------------------------------------------------------- 1 | # Retries 2 | 3 | from decorated import retries 4 | 5 | @retries(3, delay=10) 6 | def download(url): 7 | print('Downloading %s ...' % url) 8 | 9 | download('http://test.com/...') 10 | 11 | The retries decorator retries its target function on error. 12 | Maximum call times in the above example is 4 (1 original call + 3 retries) with 10 seconds interval between each retry. 13 | If all the 4 calls fail, the last error is raised. 14 | -------------------------------------------------------------------------------- /docs/advanced/chaining.md: -------------------------------------------------------------------------------- 1 | # Chaining decorators 2 | 3 | You can wrap a function with multiple decorators like this: 4 | 5 | @decorator2 6 | @decorator1 7 | def foo(a, b, c): 8 | pass 9 | 10 | This behavior is supported by standard python decorators. 11 | However, accessing the arguments from within "decorator2" is a little crumsy. 12 | Decorated solves this problem by providing a unified \_resolve\_args method (see [Accessing target function arguments](arguments.md)). 13 | -------------------------------------------------------------------------------- /docs/decorators/conditional.md: -------------------------------------------------------------------------------- 1 | # Conditional 2 | 3 | from decorated import conditional 4 | import time 5 | 6 | @conditional(condition='seconds != 0') 7 | def sleep1(seconds): 8 | time.sleep(seconds) 9 | 10 | @conditional(condition=lambda seconds: seconds != 0) 11 | def sleep2(seconds): 12 | time.sleep(seconds) 13 | 14 | The target function is called only if the condition evaluates to True. 15 | The condition argument can either be a string or a callable. 16 | -------------------------------------------------------------------------------- /test/testutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from fixtures._fixtures.tempdir import TempDir 3 | from fixtures2 import TestCase 4 | 5 | from decorated.base.function import Function 6 | from decorated.testing import DecoratedFixture 7 | 8 | 9 | class DecoratedTest(TestCase): 10 | # noinspection PyAttributeOutsideInit 11 | def setUp(self): 12 | super(DecoratedTest, self).setUp() 13 | self.tempdir = self.useFixture(TempDir()) 14 | self.decorated = DecoratedFixture() 15 | self.decorated.enable(Function) 16 | -------------------------------------------------------------------------------- /test/decorators_test/remove_extra_args_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.decorators.remove_extra_args import RemoveExtraArgs 3 | from unittest.case import TestCase 4 | 5 | @RemoveExtraArgs 6 | def foo(a, b): 7 | return a 8 | 9 | class RemoveExtraArgsTest(TestCase): 10 | def test_normal(self): 11 | self.assertEqual(1, foo(1, 2)) 12 | 13 | def test_extra_kw(self): 14 | self.assertEqual(1, foo(1, 2, timestamp=123)) 15 | self.assertEqual(1, foo(1, b=2, timestamp=123)) 16 | self.assertEqual(1, foo(a=1, b=2, timestamp=123)) 17 | -------------------------------------------------------------------------------- /src/decorated/base/thread_local.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import threading 3 | 4 | class ThreadLocal(object): 5 | def __init__(self, default=None): 6 | super(ThreadLocal, self).__init__() 7 | self._data = threading.local() 8 | self._default = default 9 | 10 | def get(self): 11 | try: 12 | return self._data.value 13 | except AttributeError: 14 | return self._default 15 | 16 | def set(self, value): 17 | self._data.value = value 18 | 19 | def reset(self): 20 | del self._data.value 21 | -------------------------------------------------------------------------------- /docs/decorators/timeout.md: -------------------------------------------------------------------------------- 1 | # Timeout 2 | 3 | from decorated import timeout 4 | 5 | @timeout(60) 6 | def download(url): 7 | print('Downloading %s ...' % url) 8 | 9 | download('http://test.com/...') 10 | 11 | A TimeoutError will be raised if download takes more than 60 seconds. 12 | 13 | The timeout decorator implements the context manager protocol. 14 | This example below achieve the same functionality. 15 | 16 | from decorated import timeout 17 | 18 | def download(url): 19 | with timeout(60): 20 | print('Downloading %s ...' % url) 21 | 22 | download('http://test.com/...') 23 | -------------------------------------------------------------------------------- /docs/advanced/wrapper.md: -------------------------------------------------------------------------------- 1 | # Wrapper Decorator 2 | 3 | Usually, you write a decorator to do something before or after calling the target function. 4 | Such behaviors can be implemented using the WrapperFunction which is a sub-class of the Function class. 5 | For example, 6 | 7 | from decorated import WrapperFunction 8 | 9 | class log(WrapperFunction): 10 | _items = {} 11 | 12 | def _before(self, *args, **kw): 13 | print('before foo') 14 | 15 | def _after(self, ret, *args, **kw): 16 | print('after foo') 17 | 18 | @log 19 | def foo(): 20 | print('inside foo') 21 | 22 | foo() 23 | -------------------------------------------------------------------------------- /docs/decorators/timeit.md: -------------------------------------------------------------------------------- 1 | # Timeit 2 | 3 | from decorated import timeit 4 | 5 | @timeit(iterations=100, repeats=3) 6 | def heavy_calc(): 7 | pass 8 | 9 | heavy_calc() 10 | 11 | This example executes heavy\_calc 100 times and measure the execution time. 12 | It repeats this process 3 times and then write the result to stdout. 13 | You can also write to loggind by specifying reporter=LOGGING_REPORTER. 14 | 15 | from decorated import timeit 16 | from decorated.util.reporters import LOGGING_REPORTER 17 | 18 | @timeit(iterations=100, repeats=3, reporter=LOGGING_REPORTER) 19 | def heavy_calc(): 20 | pass 21 | 22 | heavy_calc() 23 | -------------------------------------------------------------------------------- /docs/decorators/synchronized.md: -------------------------------------------------------------------------------- 1 | # Synchronized 2 | 3 | The effect of @synchronized is similar to its counter-part in java. 4 | For example, 5 | 6 | from decorated import synchronized 7 | from decorated.decorators.synchronized import MemoryLock 8 | 9 | lock = MemoryLock() 10 | 11 | @synchronized(lock) 12 | def save_to_db_1(): 13 | pass 14 | 15 | @synchronized(lock) 16 | def save_to_db_2(): 17 | pass 18 | 19 | The executions of save\_to\_db\_1 and save\_to\_db\_2 are synchronized to prevent conflict. 20 | You can switch from MemoryLock to FileLock (only works on linux) to synchronize among multi processes. 21 | You can also implement distributed locks using ZooKeeper. 22 | -------------------------------------------------------------------------------- /docs/decorators/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | from decorated import event 4 | 5 | upload_event = event(('filename', 'data')) 6 | 7 | @upload_event 8 | def upload(filename, data): 9 | print('saving ...') 10 | 11 | @upload_event.before 12 | def before_upload(data): 13 | print('received %d bytes' % len(data)) 14 | 15 | @upload_event.after 16 | def after_upload(filename): 17 | print('saved as %s' % filename) 18 | 19 | upload('aaa.txt', '!@#$%^&*()') 20 | 21 | In a large system, the functions are usually defined in different modules and loosely coupled by events. 22 | If functions are located in different modules, make sure you load those modules before calling the upload function. 23 | Alternatively, you can call events.init('your\_package\_name') to do this for you. -------------------------------------------------------------------------------- /test/base_test/context_test/ctx_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.context import Context, ctx, ContextError 3 | from unittest.case import TestCase 4 | 5 | class GetSetAttrTest(TestCase): 6 | def test_set(self): 7 | with Context(a=1): 8 | self.assertEquals(1, ctx.a) 9 | ctx.a = 2 10 | self.assertEquals(2, ctx.a) 11 | ctx.b = 3 12 | self.assertEquals(3, ctx.b) 13 | d = ctx.dict() 14 | self.assertEquals({'a': 2, 'b': 3}, d) 15 | 16 | class GetTest(TestCase): 17 | def test_success(self): 18 | with Context(): 19 | self.assertIsInstance(ctx.get(), Context) 20 | 21 | def test_no_context(self): 22 | with self.assertRaises(AttributeError): 23 | ctx.a 24 | -------------------------------------------------------------------------------- /src/decorated/base/proxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class Proxy(object): 4 | def __init__(self, target=None): 5 | super(Proxy, self).__init__() 6 | self._target = target 7 | 8 | def __getattr__(self, name): 9 | if name in self.__dict__: 10 | return self.__dict__[name] 11 | else: 12 | try: 13 | return getattr(self.target, name) 14 | except Exception as e: 15 | raise AttributeError("%s object has no attribute '%s'. Inner error is: [%s] %s" 16 | % (type(self).__name__, name, type(e).__name__, e)) 17 | 18 | @property 19 | def target(self): 20 | if self._target is None: 21 | raise NoTargetError() 22 | return self._target 23 | 24 | class NoTargetError(Exception): 25 | pass 26 | -------------------------------------------------------------------------------- /src/decorated/util/modutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import doctest 3 | import importlib 4 | import pkgutil 5 | import sys 6 | 7 | def load_tree(root): 8 | mod_or_pack = importlib.import_module(root) 9 | if hasattr(mod_or_pack, '__path__'): 10 | for _, mod, _ in pkgutil.walk_packages(path=mod_or_pack.__path__, prefix=mod_or_pack.__name__ + '.'): 11 | if mod not in sys.modules: 12 | importlib.import_module(mod) 13 | 14 | def module_exists(modname): 15 | ''' 16 | >>> module_exists('decorated.util.modutil') 17 | True 18 | >>> module_exists('fakepackage.fakemod') 19 | False 20 | ''' 21 | try: 22 | importlib.import_module(modname) 23 | return True 24 | except ImportError: 25 | return False 26 | 27 | if __name__ == '__main__': 28 | doctest.testmod() 29 | -------------------------------------------------------------------------------- /test/base_test/expression_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.expression import Expression, ExpressionError 3 | from testutil import DecoratedTest 4 | 5 | 6 | class ExpressionTest(DecoratedTest): 7 | def test_success(self): 8 | expression = Expression('a + b') 9 | result = expression(a=1, b=2) 10 | self.assertEqual(3, result) 11 | 12 | def test_failed(self): 13 | expression = Expression('a + c') 14 | with self.assertRaises(ExpressionError): 15 | result = expression(a=1, b=2) 16 | 17 | def test_no_access_to_builtins(self): 18 | expression = Expression('map') 19 | with self.assertRaises(ExpressionError): 20 | expression() 21 | 22 | def test_bad_syntax(self): 23 | with self.assertRaises(ExpressionError): 24 | Expression('!@#$%') 25 | -------------------------------------------------------------------------------- /src/decorated/base/expression.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.util import dutil 3 | 4 | 5 | class Expression(object): 6 | def __init__(self, string): 7 | self._string = string 8 | try: 9 | self._expression = compile(string, '', 'eval') 10 | except SyntaxError: 11 | raise ExpressionError('Bad expression "%s".' % string) 12 | 13 | def __call__(_expression_self, **variables): # variables may contain a "self" key 14 | variables = dutil.generate_safe_context(variables) 15 | try: 16 | return eval(_expression_self._expression, variables) 17 | except Exception as e: 18 | raise ExpressionError('Failed to evaluate expression "%s". Error was: %s' % (_expression_self._string, e)) 19 | 20 | def __str__(self): 21 | return self._string 22 | 23 | class ExpressionError(Exception): 24 | pass 25 | -------------------------------------------------------------------------------- /docs/advanced/method.md: -------------------------------------------------------------------------------- 1 | # Decorators on class methods 2 | 3 | Decorators can be used on class methods. For example, 4 | 5 | from decorated import Function 6 | 7 | class cache(Function): 8 | _items = {} 9 | 10 | def _decorate(self, func): 11 | self._key = func.__name__ 12 | return super(cache, self)._decorate(func) 13 | 14 | def _call(self, *args, **kw): 15 | result = self._items.get(self._key) 16 | if result is None: 17 | result = super(cache, self)._call(*args, **kw) 18 | self._items[self._key] = result 19 | return result 20 | 21 | class Config(object): 22 | @cache 23 | def get_from_db(self): 24 | print('loading config from db ...') 25 | return 'config' 26 | 27 | config = Config() 28 | print(config.get_from_db()) # will load from db 29 | print(config.get_from_db()) # will load from cache 30 | 31 | The same decorator can also be used on static methods and class methods. 32 | -------------------------------------------------------------------------------- /docs/advanced/arguments.md: -------------------------------------------------------------------------------- 1 | # Accessing target function arguments 2 | 3 | It is usually useful to get the arguments of the target function. 4 | This can be easily achieved by the \_resolve_args method. 5 | 6 | from decorated import Function 7 | 8 | class cache(Function): 9 | _items = {} 10 | 11 | def _call(self, *args, **kw): 12 | args = self._resolve_args(*args, **kw) 13 | key = args['key'] 14 | result = self._items.get(key) 15 | if result is None: 16 | result = super(cache, self)._call(*args, **kw) 17 | self._items[key] = result 18 | return result 19 | 20 | @cache 21 | def get_config_from_db(key): 22 | print('loading config from db ...') 23 | return 'config' 24 | 25 | print(get_config_from_db('config1')) # will load from db 26 | print(get_config_from_db('config1')) # will load from cache 27 | print(get_config_from_db('config2')) # will load from db 28 | print(get_config_from_db('config2')) # will load from cache 29 | -------------------------------------------------------------------------------- /test/base_test/thread_local_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.thread_local import ThreadLocal 3 | from threading import Thread 4 | from unittest.case import TestCase 5 | 6 | class ThreadLocalTest(TestCase): 7 | def test_local_thread(self): 8 | local = ThreadLocal(0) 9 | self.assertEquals(0, local.get()) 10 | local.set(1) 11 | self.assertEquals(1, local.get()) 12 | local.reset() 13 | self.assertEquals(0, local.get()) 14 | 15 | def test_another_thread(self): 16 | local = ThreadLocal(0) 17 | values = [] 18 | class AnotherThread(Thread): 19 | def run(self): 20 | values.append(local.get()) 21 | local.set(1) 22 | values.append(local.get()) 23 | local.reset() 24 | values.append(local.get()) 25 | thread = AnotherThread() 26 | thread.start() 27 | thread.join() 28 | self.assertEquals([0, 1, 0], values) 29 | -------------------------------------------------------------------------------- /src/decorated/decorators/validations/engine.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated import WrapperFunction 3 | from decorated.decorators.validations.errors import ValidationError 4 | from decorated.util import dutil 5 | 6 | 7 | class ValidationEngine(object): 8 | def __init__(self, default_error_class=ValidationError): 9 | self._default_error_class = default_error_class 10 | 11 | def rules(self, validators, error_class=None): 12 | error_class = error_class or self._default_error_class 13 | return Rules(validators, error_class) 14 | 15 | class Rules(WrapperFunction): 16 | def _before(self, *args, **kw): 17 | arg_dict = self._resolve_args(*args, **kw) 18 | for validator in self._validators: 19 | validator.validate(arg_dict, default_error_class=self._error_class) 20 | 21 | def _init(self, validators, error_class=None): 22 | super(Rules, self)._init() 23 | self._validators = dutil.listify(validators) 24 | self._error_class = error_class 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from distutils.core import setup 3 | import setuptools 4 | 5 | setup( 6 | name='decorated', 7 | version='1.9.9', 8 | author='Mengchen LEE', 9 | classifiers = [ 10 | 'Development Status :: 4 - Beta', 11 | 'Intended Audience :: Developers', 12 | 'Operating System :: OS Independent', 13 | 'Programming Language :: Python', 14 | 'Programming Language :: Python :: 2', 15 | 'Programming Language :: Python :: 3', 16 | 'Topic :: Software Development :: Libraries', 17 | ], 18 | description='Decorator framework and common decorators for python.', 19 | extras_require={ 20 | 'test': [ 21 | 'fixtures>=3.0.0', 22 | 'pylru>=1.0.9', 23 | ], 24 | }, 25 | install_requires=[ 26 | 'fixtures2>=0.1.2', 27 | 'six>=1.11.0', 28 | ], 29 | package_dir={'': 'src'}, 30 | packages=setuptools.find_packages(where='src'), 31 | url='https://github.com/cooledcoffee/decorated', 32 | ) 33 | -------------------------------------------------------------------------------- /test/decorators_test/profile_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.decorators.profile import Profile, Stats 3 | from fixtures2 import TestCase 4 | 5 | stats = None 6 | def _reporter(s): 7 | global stats 8 | stats = s 9 | 10 | class ProfileTest(TestCase): 11 | def test_basic(self): 12 | # set up 13 | @Profile(reporter=_reporter) 14 | def foo(a, b=0): 15 | foo.times += 1 16 | return 1 17 | foo.times = 0 18 | 19 | # test 20 | result = foo(1) 21 | self.assertEqual(1, result) 22 | self.assertEqual(1, foo.times) 23 | self.assertIsInstance(stats, Stats) 24 | 25 | def test_iterations(self): 26 | # set up 27 | @Profile(iterations=10, reporter=_reporter) 28 | def foo(a, b=0): 29 | foo.times += 1 30 | return 1 31 | foo.times = 0 32 | 33 | # test 34 | result = foo(1) 35 | self.assertEqual(1, result) 36 | self.assertEqual(10, foo.times) 37 | self.assertIsInstance(stats, Stats) 38 | -------------------------------------------------------------------------------- /src/decorated/decorators/validations/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from decorated.decorators.validations.engine import ValidationEngine 3 | from decorated.decorators.validations.errors import ValidationError 4 | from decorated.decorators.validations.validators.misc import ChoicesValidator, FalseValidator, NotEmptyValidator, \ 5 | NotNoneValidator, TrueValidator, TypeValidator 6 | from decorated.decorators.validations.validators.number import BetweenValidator, GeValidator, GtValidator, LeValidator, \ 7 | LtValidator, NonNegativeValidator, NumberValidator, PositiveValidator 8 | from decorated.decorators.validations.validators.string import MaxLengthValidator, RegexValidator 9 | 10 | engine = ValidationEngine() 11 | 12 | between = BetweenValidator 13 | choices = ChoicesValidator 14 | false = FalseValidator 15 | ge = GeValidator 16 | gt = GtValidator 17 | le = LeValidator 18 | lt = LtValidator 19 | max_length = MaxLengthValidator 20 | not_empty = NotEmptyValidator 21 | not_none = NotNoneValidator 22 | non_negative = NonNegativeValidator 23 | number = NumberValidator 24 | positive = PositiveValidator 25 | regex = RegexValidator 26 | true = TrueValidator 27 | type = TypeValidator 28 | -------------------------------------------------------------------------------- /src/decorated/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.false import NOTSET 3 | from decorated.base.context import Context, ctx 4 | from decorated.base.function import ContextFunction, Function, WrapperFunction, partial 5 | from decorated.decorators.conditional import Conditional 6 | from decorated.decorators.events import Event, event 7 | from decorated.decorators.files import TempDir, TempFile, WritingFile 8 | from decorated.decorators.once import Once, OnceSession 9 | from decorated.decorators.probability import Probability 10 | from decorated.decorators.profile import Profile 11 | from decorated.decorators.remove_extra_args import RemoveExtraArgs 12 | from decorated.decorators.retries import Retries, RetriesForever 13 | from decorated.decorators.timeit import TimeIt 14 | from decorated.decorators.timeout import Timeout 15 | 16 | conditional = Conditional 17 | once = Once 18 | OnceSession = OnceSession 19 | probability = Probability 20 | profile = Profile 21 | remove_extra_args = RemoveExtraArgs 22 | retries = Retries 23 | retries_forever = RetriesForever 24 | tempfile = TempFile 25 | tempdir = TempDir 26 | timeit = TimeIt 27 | timeout = Timeout 28 | writing_file = WritingFile 29 | -------------------------------------------------------------------------------- /docs/decorators/once.md: -------------------------------------------------------------------------------- 1 | Once 2 | ---- 3 | 4 | To make sure a function is called only once: 5 | 6 | from decorated import once 7 | 8 | @once 9 | def init(): 10 | pass 11 | 12 | init() # will call the function 13 | init() # will be ignored 14 | 15 | To make sure a function is called only once for a specified value: 16 | 17 | from decorated import once 18 | 19 | @once('key') 20 | def init_config(key): 21 | return 'config ' + key 22 | 23 | init_config('a') # will call the function and return 'config a' 24 | init_config('a') # will not call the function but directly return the previous result 'config a' 25 | init_config('b') # will call the function and return 'config b' 26 | 27 | To make sure a function is called only once for a given session: 28 | 29 | from decorated import once, OnceSession 30 | 31 | @once 32 | def check_is_logined(token): 33 | if token is None: 34 | raise Exception('Not authed.') 35 | 36 | with OnceSession(): 37 | check_is_logined('token') # will call the function 38 | check_is_logined('token') # will be ignored 39 | with OnceSession(): 40 | check_is_logined('token') # will call the function because it is in a different session 41 | -------------------------------------------------------------------------------- /docs/decorators/cache.md: -------------------------------------------------------------------------------- 1 | # Cache 2 | 3 | from decorated.decorators.cache import SimpleCache 4 | 5 | cache = SimpleCache() 6 | 7 | @cache.cache('/config/{key}') 8 | def get_config_from_db(key): 9 | print('loading config from db ...') 10 | return 'config' 11 | 12 | @cache.uncache('/config/{key}') 13 | def flush_config(key): 14 | print('flushing config') 15 | 16 | get_config_from_db('config1') 17 | flush_config('config1') 18 | 19 | This will cache the result with the key "/config/{key}" where "{key}" is the actual value of the key parameter. 20 | 21 | The code below implements the same function: 22 | 23 | from decorated.decorators.cache import SimpleCache 24 | 25 | cache = SimpleCache() 26 | config_cache = cache.cache('/config/{key}') 27 | 28 | @config_cache 29 | def get_config_from_db(key): 30 | print('loading config from db ...') 31 | return 'config' 32 | 33 | @config_cache.invalidate 34 | def flush_config(key): 35 | print('flushing config') 36 | 37 | get_config_from_db('config1') 38 | flush_config('config1') 39 | 40 | If you have pylru installed, you may replace SimpleCache with LruCache. 41 | You may also derive your own class from decorated.decorators.cache.Cache for memcached, redis, etc. 42 | -------------------------------------------------------------------------------- /test/decorators_test/validations_test/engine_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | 4 | from decorated.decorators.validations import TypeValidator, MaxLengthValidator 5 | from decorated.decorators.validations.engine import ValidationEngine 6 | from decorated.decorators.validations.errors import ValidationError 7 | from testutil import DecoratedTest 8 | 9 | 10 | class ValidationEngineTest(DecoratedTest): 11 | def test_single_validator(self): 12 | # set up 13 | engine = ValidationEngine() 14 | @engine.rules(TypeValidator('id', six.string_types)) 15 | def foo(id): 16 | pass 17 | 18 | # test success 19 | foo('111') 20 | 21 | # test error 22 | with self.assertRaises(ValidationError): 23 | foo(111) 24 | 25 | def test_multi_validators(self): 26 | # set up 27 | engine = ValidationEngine() 28 | @engine.rules([ 29 | TypeValidator('id', six.string_types), 30 | MaxLengthValidator('id', 4), 31 | ]) 32 | def foo(id): 33 | pass 34 | 35 | # test success 36 | foo('111') 37 | 38 | # test error 39 | with self.assertRaises(ValidationError): 40 | foo('11111') 41 | -------------------------------------------------------------------------------- /src/decorated/decorators/once.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | 4 | from decorated.base.context import ContextError, ContextMeta 5 | from decorated.base.expression import Expression 6 | from decorated.base.false import NOTSET 7 | from decorated.base.function import Function 8 | 9 | class Once(Function): 10 | def _call(self, *args, **kw): 11 | arg_dict = self._resolve_args(*args, **kw) 12 | key = self._key(**arg_dict) 13 | key = (self._func, key) 14 | 15 | try: 16 | values = OnceSession.current().values 17 | except ContextError: 18 | values = _DEFAULT_SESSION.values 19 | except AttributeError: 20 | values = _DEFAULT_SESSION.values 21 | 22 | value = values.get(key, NOTSET) 23 | if value == NOTSET: 24 | value = super(Once, self)._call(*args, **kw) 25 | values[key] = value 26 | return value 27 | 28 | def _init(self, key='None'): # pylint: disable=arguments-differ 29 | super(Once, self)._init() 30 | self._key = Expression(key) 31 | 32 | class OnceSession(six.with_metaclass(ContextMeta, object)): 33 | def __init__(self): 34 | super(OnceSession, self).__init__() 35 | self.values = {} 36 | 37 | _DEFAULT_SESSION = OnceSession() 38 | -------------------------------------------------------------------------------- /src/decorated/util/dutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from collections import Iterable 3 | 4 | import six 5 | 6 | _SAFE_BUILTINS = { 7 | '__builtins__': None, 8 | 'Exception': Exception, 9 | 'False': False, 10 | 'None': None, 11 | 'True': True, 12 | 'float': float, 13 | 'int': int, 14 | 'isinstance': isinstance, 15 | 'issubclass': issubclass, 16 | 'list': list, 17 | 'len': len, 18 | 'max': max, 19 | 'min': min, 20 | 'round': round, 21 | 'sorted': sorted, 22 | 'str': str, 23 | 'sum': sum, 24 | 'tuple': tuple, 25 | 'type': type, 26 | } 27 | if six.PY2: 28 | _SAFE_BUILTINS['unicode'] = unicode 29 | 30 | def generate_safe_context(variables): 31 | ctx = dict(_SAFE_BUILTINS) 32 | ctx.update(variables) 33 | return ctx 34 | 35 | def listify(value_or_values): 36 | ''' 37 | >>> listify(['aaa', 'bbb']) 38 | ['aaa', 'bbb'] 39 | >>> listify(('aaa', 'bbb')) 40 | ['aaa', 'bbb'] 41 | >>> listify('aaa') 42 | ['aaa'] 43 | ''' 44 | if isinstance(value_or_values, list): 45 | return value_or_values 46 | elif isinstance(value_or_values, six.string_types): 47 | return [value_or_values] 48 | elif isinstance(value_or_values, Iterable): 49 | return list(value_or_values) 50 | else: 51 | return [value_or_values] 52 | -------------------------------------------------------------------------------- /test/decorators_test/retries_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from fixtures2 import TestCase 3 | 4 | from decorated.decorators.retries import Retries 5 | 6 | 7 | class RetriesTest(TestCase): 8 | def test_success_at_first(self): 9 | @Retries(3) 10 | def foo(): 11 | return 1 12 | result = foo() 13 | self.assertEqual(1, result) 14 | 15 | def test_success_after_retry(self): 16 | @Retries(3) 17 | def foo(): 18 | foo.times += 1 19 | if foo.times == 3: 20 | return 1 21 | else: 22 | raise Exception('Failed at retry %d.' % foo.times) 23 | foo.times = 0 24 | result = foo() 25 | self.assertEqual(1, result) 26 | 27 | def test_all_failed(self): 28 | @Retries(3) 29 | def foo(): 30 | foo.times += 1 31 | raise Exception() 32 | foo.times = 0 33 | with self.assertRaises(Exception): 34 | foo() 35 | self.assertEqual(4, foo.times) 36 | 37 | def test_error_types(self): 38 | @Retries(3, error_types=(IOError, TypeError)) 39 | def foo(): 40 | foo.times += 1 41 | raise Exception() 42 | foo.times = 0 43 | with self.assertRaises(Exception): 44 | foo() 45 | self.assertEqual(1, foo.times) 46 | -------------------------------------------------------------------------------- /src/decorated/decorators/conditional.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.function import Function 3 | from decorated.decorators.remove_extra_args import RemoveExtraArgs 4 | import six 5 | 6 | class Conditional(Function): 7 | def _call(self, *args, **kw): 8 | if self._test(*args, **kw): 9 | return super(Conditional, self)._call(*args, **kw) 10 | 11 | def _init(self, condition=None): # pylint: disable=arguments-differ 12 | if hasattr(self, '_test'): 13 | _test = RemoveExtraArgs(self._test) # pylint: disable=access-member-before-definition 14 | elif isinstance(condition, six.string_types): 15 | def _test(*args, **kw): 16 | arg_dict = self._resolve_condition_args(*args, **kw) 17 | return eval(condition, arg_dict) # pylint: disable=eval-used 18 | elif callable(condition): 19 | condition = RemoveExtraArgs(condition) 20 | def _test(*args, **kw): 21 | arg_dict = self._resolve_condition_args(*args, **kw) 22 | return condition(**arg_dict) 23 | else: 24 | raise Exception('Condition can only be string or callable.') 25 | self._test = _test 26 | 27 | def _resolve_condition_args(self, *args, **kw): 28 | arg_dict = self._resolve_args(*args, **kw) 29 | if 'self' in arg_dict: 30 | arg_dict['self_'] = arg_dict.pop('self') 31 | return arg_dict 32 | -------------------------------------------------------------------------------- /src/decorated/base/template.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | import six 5 | 6 | from decorated.util import dutil 7 | 8 | if six.PY2: 9 | _DEFAULT_STRING_TYPE = 'unicode' 10 | else: 11 | _DEFAULT_STRING_TYPE = 'str' 12 | 13 | class Template(object): 14 | def __init__(self, string): 15 | self._string = string 16 | source = _generate_source(string) 17 | self._code = compile(source, '', 'exec') 18 | 19 | def __call__(_template_self, **variables): # variables may contain a "self" key 20 | variables = dutil.generate_safe_context(variables) 21 | exec(_template_self._code, variables) 22 | return variables['result'] 23 | 24 | def __str__(self): 25 | return self._string 26 | 27 | def _generate_source(string): 28 | parts = re.split('(\{.+?\})', string) 29 | for i, part in enumerate(parts): 30 | if part == '': 31 | parts[i] = None 32 | elif part.startswith('{') and part.endswith('}'): 33 | part = part[1:-1] 34 | part = ''' 35 | try: 36 | parts.append(%s(%s)) 37 | except Exception: 38 | parts.append(u'{error:%s}') 39 | ''' % (_DEFAULT_STRING_TYPE, part, part) 40 | parts[i] = part.strip() 41 | else: 42 | parts[i] = "parts.append(u'%s')" % part.replace('\n', '\\n').replace("'", "\\'") 43 | parts = '\n'.join([p for p in parts if p is not None]) 44 | source = ''' 45 | parts = [] 46 | %s 47 | result = ''.join(parts) 48 | ''' % parts 49 | return source.strip() 50 | -------------------------------------------------------------------------------- /src/decorated/decorators/retries.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import itertools 3 | import logging 4 | import time 5 | 6 | import six 7 | 8 | from decorated.base.function import Function 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | class Retries(Function): 13 | def _init(self, times, delay=0, error_types=(Exception,)): 14 | self._times = times 15 | self._delay = delay 16 | self._error_types = error_types 17 | 18 | def _call(self, *args, **kw): 19 | for i in _iter(self._times): 20 | try: 21 | return super(Retries, self)._call(*args, **kw) 22 | except Exception as e: 23 | if i > 0 and isinstance(e, self._error_types): 24 | log.warn('Execution failed. Will retry in %f seconds.', self._delay, exc_info=True) 25 | time.sleep(self._delay) 26 | else: 27 | raise 28 | 29 | class RetriesForever(Retries): 30 | def _init(self, delay=0, error_types=(Exception,)): 31 | # noinspection PyTypeChecker 32 | super(RetriesForever, self)._init(None, delay=delay, error_types=error_types) 33 | 34 | 35 | def _iter(times): 36 | ''' 37 | >>> list(_iter(3)) 38 | [3, 2, 1, 0] 39 | 40 | >>> it = _iter(None) 41 | >>> next(it) 42 | 1 43 | >>> next(it) 44 | 1 45 | ''' 46 | if times is None: 47 | return itertools.cycle([1]) 48 | else: 49 | # noinspection PyUnresolvedReferences 50 | return six.moves.xrange(times, -1, -1) 51 | -------------------------------------------------------------------------------- /src/decorated/decorators/profile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from cProfile import Profile as RawProfile 3 | from decorated.base.function import Function 4 | from decorated.util import reporters 5 | from decorated.util.gcutil import DisableGc 6 | from pstats import Stats as OriginalStats 7 | from six import StringIO 8 | import doctest 9 | 10 | class Profile(Function): 11 | def _call(self, *args, **kw): 12 | profile = RawProfile() 13 | def _run(): 14 | with DisableGc(): 15 | for _ in range(self._iterations): 16 | _run.result = super(Profile, self)._call(*args, **kw) 17 | profile.runctx('_run()', {}, {'_run': _run}) 18 | profile.create_stats() 19 | stats = Stats(profile) 20 | stats.sort_stats('cumulative') 21 | stats.fcn_list = stats.fcn_list[:self._max_lines] # pylint: disable=attribute-defined-outside-init 22 | self._reporter(stats) 23 | return _run.result 24 | 25 | def _init(self, iterations=1, reporter=reporters.PRINT_REPORTER, max_lines=50): # pylint: disable=arguments-differ 26 | super(Profile, self)._init() 27 | self._iterations = iterations 28 | self._reporter = reporter 29 | self._max_lines = max_lines 30 | 31 | class Stats(OriginalStats): 32 | def __str__(self): 33 | ''' 34 | >>> result = str(Stats(RawProfile().run(''))) 35 | >>> 'function calls' in result 36 | True 37 | ''' 38 | self.stream = StringIO() 39 | self.print_stats() 40 | return self.stream.getvalue() 41 | -------------------------------------------------------------------------------- /src/decorated/decorators/validations/validators/string.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | import six 5 | 6 | from decorated.decorators.validations.validators.misc import TypeValidator 7 | 8 | 9 | class StringValidator(TypeValidator): 10 | def __init__(self, param, error_class=None): 11 | super(StringValidator, self).__init__(param, six.string_types, error_class=error_class) 12 | 13 | class MaxLengthValidator(StringValidator): 14 | def __init__(self, param, max_length, error_class=None): 15 | super(MaxLengthValidator, self).__init__(param, error_class=error_class) 16 | self._max_length = max_length 17 | 18 | def _validate(self, value): 19 | ''' 20 | >>> MaxLengthValidator('name', 8)._validate('12345') 21 | >>> MaxLengthValidator('name', 8)._validate('123456789') 22 | 'should be less than 8 chars' 23 | ''' 24 | if len(value) > self._max_length: 25 | return 'should be less than %d chars' % self._max_length 26 | 27 | class RegexValidator(StringValidator): 28 | def __init__(self, param, regex, error_class=None): 29 | super(RegexValidator, self).__init__(param, error_class=error_class) 30 | self._regex = regex 31 | self._compiled_regex = re.compile(regex) 32 | 33 | def _validate(self, value): 34 | ''' 35 | >>> RegexValidator('name', '[a-z]+')._validate('aaa') 36 | >>> RegexValidator('name', '[a-z]+')._validate('111') 37 | 'should match regex "[a-z]+"' 38 | ''' 39 | if not self._compiled_regex.match(value): 40 | return 'should match regex "%s"' % self._regex 41 | -------------------------------------------------------------------------------- /src/decorated/decorators/timeout.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.function import ContextFunction 3 | import signal 4 | import time 5 | 6 | class Timeout(ContextFunction): 7 | def _after(self, ret, *args, **kw): 8 | self._restore() 9 | 10 | def _before(self, *args, **kw): 11 | if self._seconds is None or self._seconds == 0: 12 | return 13 | 14 | self._old_handler = signal.getsignal(signal.SIGALRM) 15 | def _timeout(*args): # pylint: disable=unused-argument 16 | raise TimeoutError() 17 | signal.signal(signal.SIGALRM, _timeout) 18 | old_alarm = signal.alarm(self._seconds) 19 | if old_alarm != 0: 20 | self._old_alarm_time = time.time() + old_alarm 21 | 22 | def _error(self, error, *args, **kw): 23 | self._restore() 24 | 25 | def _init(self, seconds): # pylint: disable=arguments-differ 26 | self._seconds = seconds 27 | self._old_handler = None 28 | self._old_alarm_time = None 29 | 30 | def _restore(self): 31 | if self._seconds is None or self._seconds == 0: 32 | return 33 | 34 | signal.alarm(0) 35 | if self._old_handler is not None: 36 | signal.signal(signal.SIGALRM, self._old_handler) 37 | if self._old_alarm_time is not None: 38 | remain_seconds = int(self._old_alarm_time - time.time()) 39 | if remain_seconds < 1: 40 | # old alarm is overdue 41 | # the best we can do is rescheduling it at 1 second later 42 | remain_seconds = 1 43 | signal.alarm(remain_seconds) 44 | 45 | class TimeoutError(Exception): 46 | pass 47 | -------------------------------------------------------------------------------- /test/decorators_test/once_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.context import Context 3 | from decorated.decorators.once import Once, OnceSession 4 | from fixtures2 import TestCase 5 | 6 | class DefaultSessionTest(TestCase): 7 | def test_no_key(self): 8 | @Once 9 | def foo(a, b): 10 | return a + b 11 | self.assertEqual(3, foo(1, 2)) 12 | self.assertEqual(3, foo(3, 4)) 13 | 14 | def test_with_key(self): 15 | @Once('a') 16 | def foo(a, b): 17 | return a + b 18 | self.assertEqual(3, foo(1, 2)) 19 | self.assertEqual(7, foo(3, 4)) 20 | self.assertEqual(3, foo(1, 2)) 21 | 22 | def test_diff_funcs(self): 23 | @Once 24 | def add(a, b): 25 | return a + b 26 | @Once 27 | def mul(a, b): 28 | return a * b 29 | self.assertEqual(3, add(1, 2)) 30 | self.assertEqual(2, mul(1, 2)) 31 | 32 | class WithSessionTest(TestCase): 33 | def test_single_level(self): 34 | @Once 35 | def foo(a, b): 36 | return a + b 37 | with OnceSession(): 38 | self.assertEqual(3, foo(1, 2)) 39 | with OnceSession(): 40 | self.assertEqual(7, foo(3, 4)) 41 | 42 | def test_multi_levels(self): 43 | @Once 44 | def foo(a, b): 45 | return a + b 46 | with Context(): 47 | with OnceSession(): 48 | with Context(): 49 | self.assertEqual(3, foo(1, 2)) 50 | with OnceSession(): 51 | with Context(): 52 | self.assertEqual(7, foo(3, 4)) 53 | 54 | def test_context_is_not_session(self): 55 | @Once 56 | def foo(a, b): 57 | return a + b 58 | with Context(): 59 | self.assertEqual(3, foo(1, 2)) 60 | self.assertEqual(3, foo(3, 4)) 61 | -------------------------------------------------------------------------------- /test/decorators_test/timeout_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.decorators.timeout import Timeout, TimeoutError 3 | from fixtures2 import TestCase 4 | import signal 5 | import time 6 | 7 | class ContextTest(TestCase): 8 | def test_within_timeout(self): 9 | with Timeout(10): 10 | pass 11 | self.assertEquals(signal.SIG_DFL, signal.getsignal(signal.SIGALRM)) 12 | 13 | def test_exceed_timeout(self): 14 | with self.assertRaises(TimeoutError): 15 | with Timeout(1): 16 | time.sleep(10) 17 | self.assertEquals(signal.SIG_DFL, signal.getsignal(signal.SIGALRM)) 18 | 19 | def test_no_timeout(self): 20 | with Timeout(0): 21 | time.sleep(0.01) 22 | self.assertEquals(signal.SIG_DFL, signal.getsignal(signal.SIGALRM)) 23 | 24 | def test_multi_alarms(self): 25 | with self.assertRaises(TimeoutError): 26 | with Timeout(3): 27 | with self.assertRaises(TimeoutError): 28 | with Timeout(1): 29 | time.sleep(10) 30 | time.sleep(10) 31 | self.assertEquals(signal.SIG_DFL, signal.getsignal(signal.SIGALRM)) 32 | 33 | def test_outer_alarm_overdue(self): 34 | with self.assertRaises(TimeoutError): 35 | with Timeout(1): 36 | with self.assertRaises(TimeoutError): 37 | with Timeout(2): 38 | time.sleep(10) 39 | time.sleep(10) 40 | self.assertEquals(signal.SIG_DFL, signal.getsignal(signal.SIGALRM)) 41 | 42 | class DecoratorTest(TestCase): 43 | def test_success(self): 44 | @Timeout(1) 45 | def foo(a, b=0): 46 | pass 47 | foo(1, b=2) 48 | 49 | def test_timeout(self): 50 | @Timeout(1) 51 | def foo(a, b=0): 52 | time.sleep(10) 53 | with self.assertRaises(TimeoutError): 54 | foo(1, b=2) 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python decorators are very powerful but sometimes a little tricky to write 2 | Decorators themself could be implemented as functions or classes. 3 | Some decorators should be called with arguments, some are not, and some can be used with or without arguments. 4 | It is useful to get the arguments of the target function in the decorators. 5 | But figuring out which aregument is for which parameter is not an easy task. 6 | Things get even messier when you try to wrap a function with multiple decorators. 7 | 8 | This is where decorated comes to rescue. 9 | Decorated solves all the above problems by providing a unified framework for writing decorators. 10 | It also provides some built-in decorators that you may find helpful to your daily work. 11 | 12 | 1\. [Basic](docs/basic.md) 13 | 14 | 2\. Advanced 15 | 16 |     2\.1\. [Accessing target function arguments](docs/advanced/arguments.md) 17 | 18 |     2\.2\. [Chaining decorators](docs/advanced/chaining.md) 19 | 20 |     2\.3\. [Decorators on class methods](docs/advanced/method.md) 21 | 22 |     2\.4\. [Wrapper Decorator](docs/advanced/wrapper.md) 23 | 24 | 3\. Built-in decorators 25 | 26 |     3\.1\. [cache](docs/decorators/cache.md) 27 | 28 |     3\.2\. [conditional](docs/decorators/conditional.md) 29 | 30 |     3\.3\. [events](docs/decorators/events.md) 31 | 32 |     3\.4\. [once](docs/decorators/once.md) 33 | 34 |     3\.5\. [profile](docs/decorators/profile.md) 35 | 36 |     3\.6\. [remove\_extra\_args](docs/decorators/remove_extra_args.md) 37 | 38 |     3\.7\. [retries](docs/decorators/retries.md) 39 | 40 |     3\.8\. [synchronized](docs/decorators/synchronized.md) 41 | 42 |     3\.9\. [timeit](docs/decorators/timeit.md) 43 | 44 |     3\.10\. [timeout](docs/decorators/timeout.md) 45 | 46 | 4\. [Loggingd](docs/loggingd.md) 47 | -------------------------------------------------------------------------------- /src/decorated/decorators/locks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import fcntl 3 | import os 4 | from threading import RLock 5 | 6 | from decorated import ContextFunction, WrapperFunction 7 | 8 | 9 | class Lock(ContextFunction): 10 | def lock(self): 11 | raise NotImplementedError() 12 | 13 | def unlock(self): 14 | raise NotImplementedError() 15 | 16 | def _after(self, ret, *args, **kw): 17 | self.unlock() 18 | 19 | def _before(self, *args, **kw): 20 | self.lock() 21 | 22 | def _decorate(self, func): 23 | return _LockProxy(self)(func) 24 | 25 | def _error(self, error, *args, **kw): 26 | self.unlock() 27 | 28 | class FileLock(Lock): 29 | def _init(self, path): # pylint: disable=arguments-differ 30 | super(FileLock, self)._init() 31 | self._path = path 32 | self._fd = None 33 | 34 | def lock(self): 35 | _create_file_if_not_exist(self._path) 36 | self._fd = os.open(self._path, os.O_RDWR) 37 | fcntl.flock(self._fd, fcntl.LOCK_EX) 38 | 39 | def unlock(self): 40 | fcntl.flock(self._fd, fcntl.LOCK_UN) 41 | 42 | class MemoryLock(Lock): 43 | def _init(self, *args, **kw): 44 | super(MemoryLock, self)._init(*args, **kw) 45 | self._lock = RLock() 46 | 47 | def lock(self): 48 | self._lock.acquire() 49 | 50 | def unlock(self): 51 | self._lock.release() 52 | 53 | class _LockProxy(WrapperFunction): 54 | def _after(self, ret, *args, **kw): 55 | self._target.unlock() 56 | 57 | def _before(self, *args, **kw): 58 | self._target.lock() 59 | 60 | def _error(self, error, *args, **kw): 61 | self._target.unlock() 62 | 63 | def _init(self, target): # pylint: disable=arguments-differ 64 | super(_LockProxy, self)._init() 65 | self._target = target 66 | 67 | def _is_init_args(self, *args, **kw): 68 | return True 69 | 70 | def _create_file_if_not_exist(path): 71 | try: 72 | f = open(path, 'w') 73 | f.close() 74 | except OSError: 75 | pass 76 | -------------------------------------------------------------------------------- /src/decorated/decorators/validations/validators/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class Validator(object): 4 | def __init__(self, param, error_class=None): 5 | self._param = param 6 | self._error_class = error_class 7 | self._validator_classes = [] 8 | self._validator_classes = _list_validator_classes(self.__class__) 9 | 10 | def validate(self, arg_dict, default_error_class=None): 11 | error_class = self._error_class or default_error_class 12 | value = self._eval_value(arg_dict, error_class) 13 | for cls in self._validator_classes: 14 | violation = cls._validate(self, value) 15 | if violation is not None: 16 | e = error_class('Arg "%s" %s, got "%s" (type=%s).' % (self._param, violation, value, type(value).__name__)) 17 | e.param = self._param 18 | raise e 19 | 20 | def _eval_value(self, arg_dict, error_class): 21 | if callable(self._param): 22 | try: 23 | value = self._param(**arg_dict) 24 | except Exception: 25 | raise error_class('Arg "%s" is missing or malformed.' % self._param) 26 | else: 27 | try: 28 | value = arg_dict[self._param] 29 | except KeyError: 30 | raise error_class('Arg "%s" is missing.' % self._param) 31 | return value 32 | 33 | def _validate(self, value): 34 | raise NotImplementedError() 35 | 36 | def _list_validator_classes(cls): 37 | ''' 38 | >>> class ParentValidator(Validator): pass 39 | >>> class ChildValidator(ParentValidator): pass 40 | >>> class GrandChildValidator(ChildValidator): pass 41 | >>> classes = _list_validator_classes(GrandChildValidator) 42 | >>> [c.__name__ for c in classes] 43 | ['ParentValidator', 'ChildValidator', 'GrandChildValidator'] 44 | ''' 45 | classes = [] 46 | while issubclass(cls, Validator) and cls is not Validator: 47 | classes.append(cls) 48 | cls = cls.__bases__[0] 49 | return tuple(reversed(classes)) 50 | -------------------------------------------------------------------------------- /test/decorators_test/locks_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | from threading import Thread 4 | from unittest.case import TestCase 5 | 6 | from decorated.decorators.locks import MemoryLock 7 | 8 | 9 | class MemoryLockTest(TestCase): 10 | def test_simple(self): 11 | lock = MemoryLock() 12 | @lock 13 | def foo(a): 14 | return a 15 | result = foo(1) 16 | self.assertEqual(1, result) 17 | 18 | def test_normal(self): 19 | # set up 20 | lock = MemoryLock() 21 | @lock 22 | def foo(): 23 | time.sleep(0.01) 24 | 25 | # test 26 | thread1 = Thread(target=foo) 27 | thread2 = Thread(target=foo) 28 | begin = time.time() 29 | thread1.start() 30 | thread2.start() 31 | thread1.join() 32 | thread2.join() 33 | end = time.time() 34 | self.assertGreater(end - begin, 0.02) 35 | 36 | def test_error(self): 37 | lock = MemoryLock() 38 | @lock 39 | def foo(): 40 | raise Exception 41 | with self.assertRaises(Exception): 42 | foo() 43 | 44 | class FileLockTest(TestCase): 45 | def test_simple(self): 46 | lock = MemoryLock() 47 | 48 | @lock 49 | def foo(a): 50 | return a 51 | 52 | result = foo(1) 53 | self.assertEqual(1, result) 54 | 55 | def test_normal(self): 56 | # set up 57 | lock = MemoryLock() 58 | 59 | @lock 60 | def foo(): 61 | time.sleep(0.01) 62 | 63 | # test 64 | thread1 = Thread(target=foo) 65 | thread2 = Thread(target=foo) 66 | begin = time.time() 67 | thread1.start() 68 | thread2.start() 69 | thread1.join() 70 | thread2.join() 71 | end = time.time() 72 | self.assertGreater(end - begin, 0.02) 73 | 74 | def test_error(self): 75 | lock = MemoryLock() 76 | 77 | @lock 78 | def foo(): 79 | raise Exception 80 | 81 | with self.assertRaises(Exception): 82 | foo() 83 | -------------------------------------------------------------------------------- /src/decorated/decorators/files.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import shutil 4 | 5 | from decorated.base.function import ContextFunction 6 | from decorated.base.template import Template 7 | 8 | 9 | class TempObject(ContextFunction): 10 | def _after(self, ret, *args, **kw): 11 | if self._delete_on_success: 12 | self._do_delete(*args, **kw) 13 | 14 | def _calc_path(self, *args, **kw): 15 | if self._func is None: 16 | return self._path() 17 | else: 18 | arg_dict = self._resolve_args(*args, **kw) 19 | return self._path(**arg_dict) 20 | 21 | def _delete(self, path): 22 | raise NotImplementedError() 23 | 24 | def _do_delete(self, *args, **kw): 25 | path = self._calc_path(*args, **kw) 26 | if not os.path.exists(path): 27 | return 28 | self._delete(path) 29 | 30 | def _error(self, error, *args, **kw): 31 | if self._delete_on_error: 32 | self._do_delete(*args, **kw) 33 | 34 | def _init(self, path, delete_on_success=True, delete_on_error=True): # pylint: disable=arguments-differ 35 | super(TempObject, self)._init() 36 | self._path = Template(path) 37 | self._delete_on_success = delete_on_success 38 | self._delete_on_error = delete_on_error 39 | 40 | class TempFile(TempObject): 41 | def _delete(self, path): 42 | os.remove(path) 43 | 44 | class TempDir(TempObject): 45 | def _delete(self, path): 46 | shutil.rmtree(path) 47 | 48 | class WritingFile(ContextFunction): 49 | def discard(self): 50 | self._discarded = True 51 | 52 | def _init(self, path): # pylint: disable=arguments-differ 53 | super(WritingFile, self)._init() 54 | self.path = path 55 | self.writing_path = self.path + '.writing' 56 | self._discarded = False 57 | 58 | def _after(self, ret, *args, **kw): 59 | if self._discarded: 60 | os.remove(self.writing_path) 61 | else: 62 | shutil.move(self.writing_path, self.path) 63 | 64 | def _error(self, error, *args, **kw): 65 | os.remove(self.writing_path) 66 | -------------------------------------------------------------------------------- /test/base_test/proxy_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.proxy import Proxy, NoTargetError 3 | from unittest.case import TestCase 4 | 5 | class TargetTest(TestCase): 6 | def test_static_success(self): 7 | target = object() 8 | proxy = Proxy(target) 9 | self.assertEqual(target, proxy.target) 10 | 11 | def test_static_none(self): 12 | proxy = Proxy(None) 13 | with self.assertRaises(NoTargetError): 14 | proxy.target 15 | 16 | def test_dynamic(self): 17 | target = object() 18 | class DynamicProxy(Proxy): 19 | @property 20 | def target(self): 21 | return target 22 | proxy = DynamicProxy() 23 | self.assertEqual(target, proxy.target) 24 | 25 | class GetAttrTest(TestCase): 26 | def test_found_in_proxy(self): 27 | class Target(object): 28 | pass 29 | class TestProxy(Proxy): 30 | def __init__(self, target): 31 | super(TestProxy, self).__init__(target) 32 | self.foo = 'foo' 33 | 34 | def bar(self): 35 | return 'bar' 36 | proxy = TestProxy(Target()) 37 | self.assertEqual('foo', proxy.foo) 38 | self.assertEqual('bar', proxy.bar()) 39 | 40 | def test_found_in_target(self): 41 | class Target(object): 42 | def __init__(self): 43 | self.foo = 'foo' 44 | 45 | def bar(self): 46 | return 'bar' 47 | proxy = Proxy(Target()) 48 | self.assertEqual('foo', proxy.foo) 49 | self.assertEqual('bar', proxy.bar()) 50 | 51 | def test_not_found(self): 52 | class Target(object): 53 | pass 54 | proxy = Proxy(Target()) 55 | with self.assertRaises(AttributeError): 56 | proxy.foo 57 | with self.assertRaises(AttributeError): 58 | proxy.bar() 59 | 60 | def test_no_target(self): 61 | proxy = Proxy() 62 | with self.assertRaises(AttributeError): 63 | proxy.foo 64 | with self.assertRaises(AttributeError): 65 | proxy.bar() 66 | -------------------------------------------------------------------------------- /test/decorators_test/validations_test/validators_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.expression import Expression 3 | from decorated.decorators.validations.errors import ValidationError 4 | from decorated.decorators.validations.validators.base import Validator 5 | from testutil import DecoratedTest 6 | 7 | 8 | class EvalValueTest(DecoratedTest): 9 | def test_basic(self): 10 | validator = Validator('key') 11 | result = validator._eval_value({'key': 'aaa'}, ValidationError) 12 | self.assertEqual('aaa', result) 13 | 14 | def test_not_found(self): 15 | validator = Validator('key') 16 | with self.assertRaises(ValidationError): 17 | validator._eval_value({}, ValidationError) 18 | 19 | def test_expression(self): 20 | validator = Validator(Expression('key.upper()')) 21 | result = validator._eval_value({'key': 'aaa'}, ValidationError) 22 | self.assertEqual('AAA', result) 23 | 24 | def test_bad_expression(self): 25 | validator = Validator(Expression('key.upper()')) 26 | with self.assertRaises(ValidationError): 27 | validator._eval_value({'key': 1}, ValidationError) 28 | 29 | class ValidateTest(DecoratedTest): 30 | def test_pass(self): 31 | class TestValidator(Validator): 32 | def _validate(self, value): 33 | return None 34 | validator = TestValidator('id', error_class=ValidationError) 35 | validator.validate({'id': None, 'name': 'aaa'}) 36 | 37 | def test_failed(self): 38 | class TestValidator(Validator): 39 | def _validate(self, value): 40 | return 'is required' 41 | validator = TestValidator('id', error_class=ValidationError) 42 | with self.assertRaises(ValidationError): 43 | validator.validate({'id': None, 'name': 'aaa'}) 44 | 45 | def test_parent_failed(self): 46 | class ParentValidator(Validator): 47 | def _validate(self, value): 48 | return 'is required' 49 | class ChildValidator(ParentValidator): 50 | def _validate(self, value): 51 | return None 52 | validator = ChildValidator('id', error_class=ValidationError) 53 | with self.assertRaises(ValidationError): 54 | validator.validate({'id': None, 'name': 'aaa'}) 55 | -------------------------------------------------------------------------------- /test/testing_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.function import WrapperFunction 3 | from decorated.decorators.conditional import Conditional 4 | from decorated.decorators.once import Once 5 | from decorated.testing import DecoratedFixture 6 | from fixtures2 import TestCase 7 | 8 | class TestWrapperFunction(WrapperFunction): 9 | called = False 10 | 11 | def _before(self, *args, **kw): 12 | TestWrapperFunction.called = True 13 | 14 | class DisableTest(TestCase): 15 | def setUp(self): 16 | super(DisableTest, self).setUp() 17 | self.decorated = self.useFixture(DecoratedFixture()) 18 | 19 | def test_with_args(self): 20 | # set up 21 | @Conditional('a != 0') 22 | def foo(a): 23 | return a 24 | self.decorated.disable(Conditional) 25 | 26 | # test 27 | self.assertEqual(0, foo(0)) 28 | self.assertEqual(1, foo(1)) 29 | 30 | def test_no_arg(self): 31 | # set up 32 | self.args = [] 33 | @Once 34 | def foo(a): 35 | self.args.append(a) 36 | return a 37 | self.decorated.disable(Once) 38 | 39 | # test 40 | self.assertEqual(1, foo(1)) 41 | self.assertEqual(1, foo(1)) 42 | self.assertEqual([1, 1], self.args) 43 | 44 | def test_method(self): 45 | # set up 46 | class Foo(object): 47 | @Conditional('a != 0') 48 | def bar(self, a): 49 | return a 50 | self.decorated.disable(Conditional) 51 | 52 | # test 53 | self.assertEqual(0, Foo().bar(0)) 54 | self.assertEqual(1, Foo().bar(1)) 55 | 56 | def test_wrapper_function(self): 57 | # set up 58 | @TestWrapperFunction 59 | def foo(a): 60 | return a 61 | self.decorated.disable(TestWrapperFunction) 62 | 63 | # test 64 | foo(0) 65 | self.assertFalse(TestWrapperFunction.called) 66 | 67 | class EnableTest(TestCase): 68 | def test_with_args(self): 69 | # set up 70 | self.decorated = self.useFixture(DecoratedFixture()) 71 | @Conditional('a != 0') 72 | def foo(a): 73 | return a 74 | self.decorated.disable(Conditional) 75 | self.decorated.enable(Conditional) 76 | 77 | # test 78 | self.assertIsNone(foo(0)) 79 | self.assertEqual(1, foo(1)) 80 | -------------------------------------------------------------------------------- /test/decorators_test/timeit_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.decorators.timeit import TimeIt 3 | from fixtures2 import TestCase 4 | 5 | timing = None 6 | def _reporter(r): 7 | global timing 8 | timing = r 9 | 10 | class TimeItTest(TestCase): 11 | def test_basic(self): 12 | # set up 13 | @TimeIt(reporter=_reporter) 14 | def foo(a, b=0): 15 | return 1 16 | 17 | # test 18 | result = foo(1) 19 | self.assertEqual(1, result) 20 | self.assertEqual(1, timing.iterations) 21 | self.assertEqual(1, timing.repeats) 22 | self.assertEqual(1, len(timing.timings)) 23 | 24 | def test_ratio(self): 25 | # set up 26 | @TimeIt(ratio=0, reporter=_reporter) 27 | def foo(a, b=0): 28 | return 1 29 | 30 | # test 31 | result = foo(1) 32 | self.assertEqual(1, result) 33 | self.assertIsNotNone(timing) 34 | 35 | def test_iterations(self): 36 | # set up 37 | @TimeIt(iterations=10, reporter=_reporter) 38 | def foo(a, b=0): 39 | foo.times += 1 40 | return 1 41 | foo.times = 0 42 | 43 | # test 44 | result = foo(1) 45 | self.assertEqual(1, result) 46 | self.assertEqual(10, foo.times) 47 | self.assertEqual(10, timing.iterations) 48 | self.assertEqual(1, timing.repeats) 49 | self.assertEqual(1, len(timing.timings)) 50 | 51 | def test_repeats(self): 52 | # set up 53 | @TimeIt(repeats=10, reporter=_reporter) 54 | def foo(a, b=0): 55 | foo.times += 1 56 | return 1 57 | foo.times = 0 58 | 59 | # test 60 | result = foo(1) 61 | self.assertEqual(1, result) 62 | self.assertEqual(10, foo.times) 63 | self.assertEqual(1, timing.iterations) 64 | self.assertEqual(10, timing.repeats) 65 | self.assertEqual(10, len(timing.timings)) 66 | 67 | def test_iterations_and_repeats(self): 68 | # set up 69 | @TimeIt(iterations=10, repeats=10, reporter=_reporter) 70 | def foo(a, b=0): 71 | foo.times += 1 72 | return 1 73 | foo.times = 0 74 | 75 | # test 76 | result = foo(1) 77 | self.assertEqual(1, result) 78 | self.assertEqual(100, foo.times) 79 | self.assertEqual(10, timing.iterations) 80 | self.assertEqual(10, timing.repeats) 81 | self.assertEqual(10, len(timing.timings)) 82 | -------------------------------------------------------------------------------- /test/base_test/function_test/partial_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.function import Function, partial 3 | from unittest.case import TestCase 4 | 5 | class FullDecorator(Function): 6 | def _init(self, a, b, c=None): 7 | self.a = a 8 | self.b = b 9 | self.c = c 10 | 11 | def foo(d, e, f=None): 12 | return d + e + f 13 | 14 | class StrTest(TestCase): 15 | def test(self): 16 | partial_decorator = partial(FullDecorator) 17 | decorated = partial_decorator(1, 2, 3)(foo) 18 | self.assertEqual('', str(decorated)) 19 | 20 | class InitTest(TestCase): 21 | def test_with_nothing(self): 22 | partial_decorator = partial(FullDecorator) 23 | decorated = partial_decorator(1, 2, 3)(foo) 24 | self.assertEqual(1, decorated.a) 25 | self.assertEqual(2, decorated.b) 26 | self.assertEqual(3, decorated.c) 27 | 28 | def test_with_args(self): 29 | partial_decorator = partial(FullDecorator, init_args=(1, 2)) 30 | decorated = partial_decorator(3)(foo) 31 | self.assertEqual(1, decorated.a) 32 | self.assertEqual(2, decorated.b) 33 | self.assertEqual(3, decorated.c) 34 | 35 | def test_with_kw(self): 36 | partial_decorator = partial(FullDecorator, init_kw={'c': 3}) 37 | decorated = partial_decorator(1, 2)(foo) 38 | self.assertEqual(1, decorated.a) 39 | self.assertEqual(2, decorated.b) 40 | self.assertEqual(3, decorated.c) 41 | 42 | class CallTest(TestCase): 43 | def test_with_nothing(self): 44 | partial_decorator = partial(FullDecorator) 45 | decorated = partial_decorator(1, 2, 3)(foo) 46 | self.assertEqual(('d', 'e', 'f'), decorated.params) 47 | result = decorated(4, 5, 6) 48 | self.assertEqual(15, result) 49 | 50 | def test_with_args(self): 51 | partial_decorator = partial(FullDecorator, call_args=(4, 5)) 52 | decorated = partial_decorator(1, 2, 3)(foo) 53 | self.assertEqual(('f',), decorated.params) 54 | result = decorated(6) 55 | self.assertEqual(15, result) 56 | 57 | def test_with_kw(self): 58 | partial_decorator = partial(FullDecorator, call_kw={'f': 6}) 59 | decorated = partial_decorator(1, 2, 3)(foo) 60 | self.assertEqual(('d', 'e'), decorated.params) 61 | self.assertEqual(('d', 'e'), decorated.required_params) 62 | self.assertEqual((), decorated.optional_params) 63 | result = decorated(4, 5) 64 | self.assertEqual(15, result) 65 | -------------------------------------------------------------------------------- /src/decorated/base/dict.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import doctest 3 | 4 | class Dict(dict): 5 | ''' 6 | >>> d = Dict(a=1) 7 | 8 | >>> 'a' in d 9 | True 10 | >>> d['a'] 11 | 1 12 | >>> d.a 13 | 1 14 | 15 | >>> d.a = 2 16 | >>> d['a'] 17 | 2 18 | >>> d.a 19 | 2 20 | 21 | >>> d['b'] = 2 22 | >>> 'b' in d 23 | True 24 | >>> d['b'] 25 | 2 26 | >>> d.b 27 | 2 28 | 29 | >>> d.c = 3 30 | >>> 'c' in d 31 | True 32 | >>> d['c'] 33 | 3 34 | >>> d.c 35 | 3 36 | 37 | >>> del d.c 38 | >>> 'c' in d 39 | False 40 | >>> d['c'] 41 | Traceback (most recent call last): 42 | ... 43 | KeyError: 'c' 44 | >>> d.c 45 | Traceback (most recent call last): 46 | ... 47 | AttributeError: 'c' 48 | 49 | >>> del d.d 50 | Traceback (most recent call last): 51 | ... 52 | AttributeError: 'd' 53 | ''' 54 | def __getattr__(self, name): 55 | try: 56 | return super(Dict, self).__getitem__(name) 57 | except KeyError as e: 58 | raise AttributeError(str(e)) 59 | 60 | def __setattr__(self, name, value): 61 | super(Dict, self).__setitem__(name, value) 62 | 63 | def __delattr__(self, name): 64 | try: 65 | super(Dict, self).__delitem__(name) 66 | except KeyError as e: 67 | raise AttributeError(str(e)) 68 | 69 | class DefaultDict(Dict): 70 | ''' 71 | >>> d = DefaultDict(lambda k: 2 * k) 72 | >>> d['a'] 73 | 'aa' 74 | >>> 'a' in d 75 | True 76 | >>> d.b 77 | 'bb' 78 | >>> 'b' in d 79 | True 80 | 81 | >>> d = DefaultDict() 82 | >>> d.a is None 83 | True 84 | 85 | >>> d = DefaultDict(a=1, b=2) 86 | >>> d.a 87 | 1 88 | 89 | >>> d = DefaultDict() 90 | >>> len(d) 91 | 0 92 | >>> len(d.items()) 93 | 0 94 | >>> len(d.keys()) 95 | 0 96 | >>> len(d.values()) 97 | 0 98 | >>> '_default' in d 99 | False 100 | ''' 101 | def __init__(self, default=None, **kw): 102 | super(DefaultDict, self).__init__(**kw) 103 | dict.__setattr__(self, '_default', default or (lambda key: None)) 104 | 105 | def __getattr__(self, key): 106 | return self.__getitem__(key) 107 | 108 | def __getitem__(self, key): 109 | if key not in self: 110 | self[key] = self._default(key) 111 | return super(DefaultDict, self).__getitem__(key) 112 | 113 | if __name__ == '__main__': 114 | doctest.testmod() 115 | -------------------------------------------------------------------------------- /test/base_test/function_test/wrapper_function_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.function import WrapperFunction 3 | from fixtures2 import TestCase 4 | 5 | class BeforeTest(TestCase): 6 | def test(self): 7 | # set up 8 | class TestFunction(WrapperFunction): 9 | def _before(self, *args, **kw): 10 | BeforeTest.args = self._resolve_args(*args, **kw) 11 | @TestFunction 12 | def foo(a, b=0): 13 | return 1 14 | 15 | # test 16 | result = foo(1, b=2) 17 | self.assertEqual(1, result) 18 | self.assertEqual({'a': 1, 'b': 2}, self.args) 19 | 20 | class AfterTest(TestCase): 21 | def test_success(self): 22 | # set up 23 | class TestFunction(WrapperFunction): 24 | def _after(self, ret, *args, **kw): 25 | AfterTest.args = self._resolve_args(*args, **kw) 26 | AfterTest.ret = ret 27 | @TestFunction 28 | def foo(a, b=0): 29 | return 1 30 | 31 | # test 32 | result = foo(1, b=2) 33 | self.assertEqual(1, result) 34 | self.assertEqual({'a': 1, 'b': 2}, self.args) 35 | self.assertEqual(1, self.ret) 36 | 37 | def test_error(self): 38 | # set up 39 | AfterTest.called = False 40 | class TestFunction(WrapperFunction): 41 | def _after(self, ret, error, *args, **kw): 42 | AfterTest.called = True 43 | @TestFunction 44 | def foo(a, b=0): 45 | raise Exception() 46 | 47 | # test 48 | with self.assertRaises(Exception): 49 | foo(1, b=2) 50 | self.assertFalse(self.called) 51 | 52 | class ErrorTest(TestCase): 53 | def test_success(self): 54 | # set up 55 | ErrorTest.called = False 56 | class TestFunction(WrapperFunction): 57 | def _error(self, err, *args, **kw): 58 | AfterTest.called = True 59 | @TestFunction 60 | def foo(a, b=0): 61 | return 1 62 | 63 | # test 64 | result = foo(1, b=2) 65 | self.assertEqual(1, result) 66 | self.assertFalse(self.called) 67 | 68 | def test_error(self): 69 | # set up 70 | class TestFunction(WrapperFunction): 71 | def _error(self, err, *args, **kw): 72 | ErrorTest.err = err 73 | ErrorTest.args = self._resolve_args(*args, **kw) 74 | @TestFunction 75 | def foo(a, b=0): 76 | raise Exception() 77 | 78 | # test 79 | with self.assertRaises(Exception): 80 | foo(1, b=2) 81 | self.assertIsInstance(self.err, Exception) 82 | self.assertEqual({'a': 1, 'b': 2}, self.args) 83 | -------------------------------------------------------------------------------- /test/base_test/function_test/context_function_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.function import ContextFunction 3 | from fixtures2 import TestCase 4 | 5 | class CallTest(TestCase): 6 | def test_success(self): 7 | # set up 8 | class TestFunction(ContextFunction): 9 | def _before(self): 10 | CallTest.before_called = True 11 | def _after(self, ret): 12 | CallTest.after_called = True 13 | CallTest.ret = ret 14 | @TestFunction 15 | def foo(): 16 | return 1 17 | 18 | # test 19 | result = foo() 20 | self.assertEqual(1, result) 21 | self.assertTrue(self.before_called) 22 | self.assertTrue(self.after_called) 23 | self.assertEqual(1, self.ret) 24 | 25 | def test_error(self): 26 | # set up 27 | class TestFunction(ContextFunction): 28 | def _before(self): 29 | CallTest.before_called = True 30 | def _error(self, error): 31 | CallTest.error_called = True 32 | CallTest.error = error 33 | @TestFunction 34 | def foo(): 35 | raise Exception 36 | 37 | # test 38 | with self.assertRaises(Exception): 39 | foo() 40 | self.assertTrue(self.before_called) 41 | self.assertTrue(self.error_called) 42 | self.assertIsInstance(self.error, Exception) 43 | 44 | class WithTest(TestCase): 45 | def test_success(self): 46 | # set up 47 | class TestFunction(ContextFunction): 48 | def _before(self, *args, **kw): 49 | WithTest.before_called = True 50 | def _after(self, ret, *args, **kw): 51 | WithTest.after_called = True 52 | 53 | # test 54 | with TestFunction(): 55 | pass 56 | self.assertTrue(self.before_called) 57 | self.assertTrue(self.after_called) 58 | 59 | def test_error(self): 60 | # set up 61 | class TestFunction(ContextFunction): 62 | def _before(self, *args, **kw): 63 | WithTest.before_called = True 64 | def _error(self, error, *args, **kw): 65 | WithTest.error_called = True 66 | WithTest.error = error 67 | 68 | # test 69 | with self.assertRaises(Exception): 70 | with TestFunction(): 71 | raise Exception() 72 | self.assertTrue(self.before_called) 73 | self.assertTrue(self.error_called) 74 | self.assertIsInstance(self.error, Exception) 75 | 76 | def test_init(self): 77 | # set up 78 | class TestFunction(ContextFunction): 79 | def _init(self, a): 80 | WithTest.a = a 81 | 82 | # test 83 | with TestFunction(1): 84 | pass 85 | self.assertEqual(1, self.a) 86 | -------------------------------------------------------------------------------- /src/decorated/decorators/timeit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | import sys 4 | import time 5 | 6 | from decorated.base.function import Function 7 | from decorated.util import reporters 8 | from decorated.util.gcutil import DisableGc 9 | 10 | if sys.platform == "win32": 11 | # On Windows, the best timer is time.clock() 12 | timer = time.clock 13 | else: 14 | # On most other platforms the best timer is time.time() 15 | timer = time.time 16 | 17 | class TimeIt(Function): 18 | def _call(self, *args, **kw): 19 | if random.random() < self._ratio: 20 | timings = [None] * self._repeats 21 | with DisableGc(): 22 | for i in range(self._repeats): 23 | begin = timer() 24 | for _ in range(self._iterations): 25 | result = super(TimeIt, self)._call(*args, **kw) 26 | seconds = timer() - begin 27 | timings[i] = seconds 28 | timing = Result(self._func, args, kw, self._iterations, 29 | self._repeats, timings) 30 | self._reporter(timing) 31 | return result 32 | else: 33 | return super(TimeIt, self)._call(*args, **kw) 34 | 35 | def _init(self, iterations=1, ratio=1, repeats=1, reporter=reporters.PRINT_REPORTER): # pylint: disable=arguments-differ 36 | super(TimeIt, self)._init() 37 | self._iterations = iterations 38 | self._ratio = ratio 39 | self._repeats = repeats 40 | self._reporter = reporter 41 | 42 | class Result(object): 43 | def __init__(self, func, args, kwargs, iterations, repeats, timings): # pylint: disable=too-many-arguments 44 | self.func = func 45 | self.args = args 46 | self.kwargs = kwargs 47 | self.iterations = iterations 48 | self.repeats = repeats 49 | self.timings = timings 50 | 51 | def __str__(self): 52 | ''' 53 | basic 54 | >>> def foo(): pass 55 | >>> foo.__module__ = 'aaa.bbb' 56 | >>> str(Result(foo, [], {}, 10, 3, [0.05, 0.1, 0.15])) 57 | 'Timing of aaa.bbb.foo(): [0.05, 0.1, 0.15] (iterations=10)' 58 | 59 | with args 60 | >>> def foo(a): pass 61 | >>> foo.__module__ = 'aaa.bbb' 62 | >>> str(Result(foo, [1], {}, 10, 3, [0.05, 0.1, 0.15])) 63 | 'Timing of aaa.bbb.foo(1): [0.05, 0.1, 0.15] (iterations=10)' 64 | 65 | with kwargs 66 | >>> def foo(a, b=0): pass 67 | >>> foo.__module__ = 'aaa.bbb' 68 | >>> str(Result(foo, [1], {'b': 2}, 10, 3, [0.05, 0.1, 0.15])) 69 | 'Timing of aaa.bbb.foo(1, b=2): [0.05, 0.1, 0.15] (iterations=10)' 70 | ''' 71 | kwargs = ['%s=%s' % (k, v) for k, v in self.kwargs.items()] 72 | args = [str(arg) for arg in self.args] + kwargs 73 | args = ', '.join(args) 74 | return 'Timing of %s.%s(%s): %s (iterations=%d)' \ 75 | % (self.func.__module__, self.func.__name__, args, self.timings, self.iterations) 76 | -------------------------------------------------------------------------------- /test/decorators_test/files_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.decorators.files import TempFile, WritingFile, TempDir 3 | from testutil import DecoratedTest 4 | import os 5 | 6 | class TempFileTest(DecoratedTest): 7 | def setUp(self): 8 | super(TempFileTest, self).setUp() 9 | self.path = self.tempdir.join('111') 10 | 11 | def test_never_created(self): 12 | with TempFile(self.path): 13 | pass 14 | self.assertFalse(os.path.exists(self.path)) 15 | 16 | def test_already_deleted(self): 17 | _touch(self.path) 18 | with TempFile(self.path): 19 | os.remove(self.path) 20 | self.assertFalse(os.path.exists(self.path)) 21 | 22 | def test_not_deleted(self): 23 | _touch(self.path) 24 | with TempFile(self.path): 25 | pass 26 | self.assertFalse(os.path.exists(self.path)) 27 | 28 | def test_not_delete_on_success(self): 29 | _touch(self.path) 30 | with TempFile(self.path, delete_on_success=False): 31 | pass 32 | self.assertTrue(os.path.exists(self.path)) 33 | 34 | def test_not_delete_on_failure(self): 35 | _touch(self.path) 36 | try: 37 | with TempFile(self.path, delete_on_error=False): 38 | raise Exception() 39 | except: 40 | pass 41 | self.assertTrue(os.path.exists(self.path)) 42 | 43 | def test_decorator(self): 44 | @TempFile('{path}') 45 | def create_file(path): 46 | _touch(path) 47 | create_file(self.path) 48 | self.assertFalse(os.path.exists(self.path)) 49 | 50 | class TempDirTest(DecoratedTest): 51 | def test(self): 52 | dirname = self.tempdir.join('111') 53 | with TempDir(dirname): 54 | os.mkdir(dirname) 55 | self.assertFalse(os.path.exists(dirname)) 56 | 57 | class WritingFileTest(DecoratedTest): 58 | def test_success(self): 59 | path = self.tempdir.join('111') 60 | with WritingFile(path) as wf: 61 | with open(wf.writing_path, 'w') as f: 62 | f.write('aaa') 63 | self.assertFalse(os.path.exists(wf.writing_path)) 64 | self.assertEqual('aaa', open(path).read()) 65 | 66 | def test_error(self): 67 | path = self.tempdir.join('111') 68 | with self.assertRaises(Exception): 69 | with WritingFile(path) as wf: 70 | with open(wf.writing_path, 'w') as f: 71 | f.write('aaa') 72 | raise Exception() 73 | self.assertFalse(os.path.exists(wf.writing_path)) 74 | self.assertFalse(os.path.exists(path)) 75 | 76 | def test_discard(self): 77 | path = self.tempdir.join('111') 78 | with WritingFile(path) as wf: 79 | with open(wf.writing_path, 'w') as f: 80 | f.write('aaa') 81 | wf.discard() 82 | self.assertFalse(os.path.exists(wf.writing_path)) 83 | self.assertFalse(os.path.exists(path)) 84 | 85 | def _touch(path): 86 | f = open(path, 'w') 87 | f.close() 88 | -------------------------------------------------------------------------------- /test/base_test/context_test/context_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.context import Context 3 | from unittest.case import TestCase 4 | 5 | class WithTest(TestCase): 6 | def test_single_level(self): 7 | with Context(path='/test'): 8 | self.assertEqual('/test', Context.current().path) 9 | 10 | def test_multi_levels(self): 11 | with Context(a=1, b=2): 12 | self.assertEqual(1, Context.current().a) 13 | self.assertEqual(2, Context.current().b) 14 | with Context(b=3, c=4): 15 | self.assertEqual(1, Context.current().a) 16 | self.assertEqual(3, Context.current().b) 17 | self.assertEqual(4, Context.current().c) 18 | self.assertEqual(1, Context.current().a) 19 | self.assertEqual(2, Context.current().b) 20 | with self.assertRaises(AttributeError): 21 | Context.current().c 22 | 23 | def test_multi_levels_method(self): 24 | class Context1(Context): 25 | def a(self): 26 | return 1 27 | 28 | def b(self): 29 | return 2 30 | class Context2(Context): 31 | def b(self): 32 | return 3 33 | 34 | def c(self): 35 | return 4 36 | with Context1(): 37 | self.assertEqual(1, Context.current().a()) 38 | self.assertEqual(2, Context.current().b()) 39 | with Context2(): 40 | self.assertEqual(1, Context.current().a()) 41 | self.assertEqual(3, Context.current().b()) 42 | self.assertEqual(4, Context.current().c()) 43 | self.assertEqual(1, Context.current().a()) 44 | self.assertEqual(2, Context.current().b()) 45 | with self.assertRaises(AttributeError): 46 | self.assertEqual(3, Context.current().c()) 47 | 48 | class DictTest(TestCase): 49 | def test_single_level(self): 50 | ctx = Context(a=1, b=2, _c=3) 51 | data = ctx.dict() 52 | self.assertEqual({'a': 1, 'b': 2}, data) 53 | 54 | def test_multi_levels(self): 55 | with Context(a=1, b=2, _c=3): 56 | with Context(b=3, _d=4) as ctx: 57 | data = ctx.dict() 58 | self.assertEqual({'a': 1, 'b': 3}, data) 59 | 60 | class DeferTest(TestCase): 61 | def test_normal(self): 62 | self.calls = [] 63 | def _action1(): 64 | self.calls.append('action1') 65 | def _action2(): 66 | self.calls.append('action2') 67 | with Context() as ctx: 68 | ctx.defer(_action1) 69 | ctx.defer(_action2) 70 | self.assertEqual(['action1', 'action2'], self.calls) 71 | 72 | def test_error(self): 73 | self.calls = [] 74 | def _action1(): 75 | raise Exception 76 | def _action2(): 77 | self.calls.append('action2') 78 | with Context() as ctx: 79 | ctx.defer(_action1) 80 | ctx.defer(_action2) 81 | self.assertEqual(['action2'], self.calls) 82 | -------------------------------------------------------------------------------- /src/decorated/decorators/validations/validators/misc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from collections import Iterable 3 | 4 | from decorated.base.expression import Expression 5 | 6 | from decorated.decorators.validations.validators.base import Validator 7 | 8 | 9 | class ChoicesValidator(Validator): 10 | def __init__(self, param, choices, error_class=None): 11 | super(ChoicesValidator, self).__init__(param, error_class=error_class) 12 | self._choices = choices 13 | 14 | def _validate(self, value): 15 | ''' 16 | >>> ChoicesValidator('id', [1, 2, 3])._validate(2) 17 | >>> ChoicesValidator('id', [1, 2, 3])._validate(0) 18 | 'should be one of [1, 2, 3]' 19 | >>> ChoicesValidator('id', (1, 2, 3))._validate(0) 20 | 'should be one of (1, 2, 3)' 21 | ''' 22 | if value not in self._choices: 23 | return 'should be one of %s' % (self._choices,) 24 | 25 | 26 | class FalseValidator(Validator): 27 | def _validate(self, value): 28 | ''' 29 | >>> FalseValidator(Expression('a > b'))._validate(False) 30 | >>> FalseValidator(Expression('a > b'))._validate(True) 31 | 'should be false' 32 | ''' 33 | if value: 34 | return 'should be false' 35 | 36 | 37 | class NotNoneValidator(Validator): 38 | def _validate(self, value): 39 | ''' 40 | >>> NotNoneValidator('id')._validate(111) 41 | >>> NotNoneValidator('id')._validate(None) 42 | 'should not be none' 43 | ''' 44 | if value is None: 45 | return 'should not be none' 46 | 47 | 48 | class NotEmptyValidator(Validator): 49 | def _validate(self, value): 50 | ''' 51 | >>> NotEmptyValidator('values')._validate(111) 52 | >>> NotEmptyValidator('values')._validate(True) 53 | >>> NotEmptyValidator('values')._validate(False) 54 | >>> NotEmptyValidator('values')._validate(None) 55 | 'should not be empty' 56 | >>> NotEmptyValidator('values')._validate([]) 57 | 'should not be empty' 58 | >>> NotEmptyValidator('values')._validate('') 59 | 'should not be empty' 60 | ''' 61 | if not value and not isinstance(value, bool): 62 | return 'should not be empty' 63 | 64 | 65 | class TrueValidator(Validator): 66 | def _validate(self, value): 67 | ''' 68 | >>> TrueValidator(Expression('a > b'))._validate(True) 69 | >>> TrueValidator(Expression('a > b'))._validate(False) 70 | 'should be true' 71 | ''' 72 | if not value: 73 | return 'should be true' 74 | 75 | 76 | class TypeValidator(Validator): 77 | def __init__(self, param, types, error_class=None): 78 | super(TypeValidator, self).__init__(param, error_class=error_class) 79 | self._types = types 80 | if isinstance(types, Iterable): 81 | self._types_string = '/'.join([t.__name__ for t in types]) 82 | else: 83 | self._types_string = types.__name__ 84 | 85 | def _validate(self, value): 86 | ''' 87 | >>> TypeValidator('value', int)._validate(111) 88 | >>> TypeValidator('value', int)._validate('111') 89 | 'should be int' 90 | >>> TypeValidator('value', (int, float))._validate('111') 91 | 'should be int/float' 92 | ''' 93 | if not isinstance(value, self._types): 94 | return 'should be %s' % self._types_string 95 | -------------------------------------------------------------------------------- /src/decorated/decorators/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | 4 | from decorated.base.function import Function 5 | from decorated.base.template import Template 6 | from decorated.util import modutil 7 | 8 | 9 | class _CacheDecorator(Function): 10 | @property 11 | def invalidate(self): 12 | return self._cache.uncache(self._key, **self._vars) 13 | 14 | def _call(self, *args, **kw): 15 | arg_dict = dict(self._vars) 16 | arg_dict.update(self._resolve_args(*args, **kw)) 17 | key = self._key(**arg_dict) 18 | return self._process(key, *args, **kw) 19 | 20 | def _init(self, cache, key, vars=None, options=None): # pylint: disable=arguments-differ 21 | super(_CacheDecorator, self)._init() 22 | self._cache = cache 23 | self._key = key if callable(key) else Template(key) 24 | self._vars = vars or {} 25 | self._options = options 26 | 27 | def _process(self, key, *args, **kw): 28 | raise NotImplementedError() 29 | 30 | class Cache(object): 31 | def cache(self, key, vars=None, **options): 32 | class _Decorator(_CacheDecorator): 33 | def _process(self, key, *args, **kw): 34 | result = self._cache.get(key, **options) 35 | if result is None: 36 | result = self._func(*args, **kw) 37 | self._cache.set(key, result, **options) 38 | return result 39 | return _Decorator(self, key, vars=vars, options=options) 40 | 41 | def clear(self): 42 | raise NotImplementedError() 43 | 44 | def delete(self, key, **options): 45 | raise NotImplementedError() 46 | 47 | def get(self, key, **options): 48 | raise NotImplementedError() 49 | 50 | def set(self, key, value, **options): 51 | raise NotImplementedError() 52 | 53 | def uncache(self, key, vars=None, **options): 54 | class _Decorator(_CacheDecorator): 55 | def _process(self, key, *args, **kw): 56 | self._cache.delete(key, **options) 57 | return self._func(*args, **kw) 58 | return _Decorator(self, key, vars=vars, options=options) 59 | 60 | class SimpleCache(Cache): 61 | def __init__(self): 62 | super(SimpleCache, self).__init__() 63 | self.clear() 64 | 65 | def clear(self): 66 | self._data = {} 67 | 68 | def delete(self, key, **options): 69 | del self._data[key] 70 | 71 | def get(self, key, **options): 72 | value, expires = self._data.get(key, (None, None)) 73 | if expires is None or expires < time.time(): 74 | return None 75 | else: 76 | return value 77 | 78 | def set(self, key, value, **options): 79 | expires = time.time() + options.get('ttl', 3600) 80 | self._data[key] = (value, expires) 81 | 82 | if modutil.module_exists('pylru'): 83 | from pylru import lrucache 84 | 85 | class LruCache(Cache): 86 | def __init__(self, size=1000): 87 | self._size = size 88 | self.clear() 89 | 90 | def clear(self): 91 | self._cache = lrucache(self._size) 92 | 93 | def delete(self, key, **options): 94 | del self._cache[key] 95 | 96 | def get(self, key, **options): 97 | try: 98 | return self._cache[key] 99 | except KeyError: 100 | return None 101 | 102 | def set(self, key, value, **options): 103 | self._cache[key] = value 104 | -------------------------------------------------------------------------------- /docs/basic.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | The workflow of the @cache decorator can be decomposed into three phases: 4 | 5 | 0. init: 6 | A new instance of the cache class is instantiated. 7 | The \_init method is called along with the init arguments. 8 | You can omit this method if your decorator does not have parameter. 9 | 10 | 0. decorate: 11 | The \_decorate method is called along with the target function. 12 | The return value is used as the decorator. 13 | Normally, you get this return value by calling Function.\_decorate method. 14 | 15 | 0. call: 16 | Whenever the decorated function is called, 17 | the execution is proxied to the \_call method with the actual arguments. 18 | 19 | ## Example 1: Decorator without argument 20 | 21 | from decorated import Function 22 | 23 | class cache(Function): 24 | _items = {} 25 | 26 | def _decorate(self, func): 27 | self._key = func.__name__ 28 | return super(cache, self)._decorate(func) 29 | 30 | def _call(self, *args, **kw): 31 | result = self._items.get(self._key) 32 | if result is None: 33 | result = super(cache, self)._call(*args, **kw) 34 | self._items[self._key] = result 35 | return result 36 | 37 | @cache 38 | def load_config_from_db(): 39 | print('loading config from db ...') 40 | return 'config' 41 | 42 | print(load_config_from_db()) # will load from db 43 | print(load_config_from_db()) # will load from cache 44 | 45 | ## Example 2: Decorator with arguments 46 | 47 | from decorated import Function 48 | 49 | class cache(Function): 50 | _items = {} 51 | 52 | def _init(self, key=None): 53 | self._key = key 54 | 55 | def _call(self, *args, **kw): 56 | result = self._items.get(self._key) 57 | if result is None: 58 | result = super(cache, self)._call(*args, **kw) 59 | self._items[self._key] = result 60 | return result 61 | 62 | @cache('config') 63 | def load_config_from_db(): 64 | print('loading config from db ...') 65 | return 'config' 66 | 67 | @cache('data') 68 | def load_data_from_db(): 69 | print('loading data from db ...') 70 | return 'data' 71 | 72 | print(load_config_from_db()) # will load from db 73 | print(load_config_from_db()) # will load from cache 74 | print(load_data_from_db()) # will load from db 75 | print(load_data_from_db()) # will load from cache 76 | 77 | ## Example 3: Decorator that can be used with or without arguments 78 | 79 | from decorated import Function 80 | 81 | class cache(Function): 82 | _items = {} 83 | 84 | def _init(self, key=None): 85 | self._key = key 86 | 87 | def _decorate(self, func): 88 | if self._key is None: 89 | self._key = func.__name__ 90 | return super(cache, self)._decorate(func) 91 | 92 | def _call(self, *args, **kw): 93 | result = self._items.get(self._key) 94 | if result is None: 95 | result = super(cache, self)._call(*args, **kw) 96 | self._items[self._key] = result 97 | return result 98 | 99 | @cache 100 | def load_config1_from_db(): 101 | print('loading config1 from db ...') 102 | return 'config1' 103 | 104 | @cache('config2') 105 | def load_config2_from_db(): 106 | print('loading config2 from db ...') 107 | return 'config2' 108 | 109 | print(load_config1_from_db()) # will load from db 110 | print(load_config1_from_db()) # will load from cache 111 | print(load_config2_from_db()) # will load from db 112 | print(load_config2_from_db()) # will load from cache 113 | -------------------------------------------------------------------------------- /test/decorators_test/conditional_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.decorators.conditional import Conditional 3 | from fixtures2 import TestCase 4 | 5 | class FunctionTest(TestCase): 6 | def test_string(self): 7 | @Conditional(condition='a == 1') 8 | def foo(a, b): 9 | return a 10 | self.assertEqual(1, foo(1, 1)) 11 | self.assertIsNone(foo(2, 2)) 12 | 13 | def test_lambda(self): 14 | @Conditional(condition=lambda a: a == 1) 15 | def foo(a, b): 16 | return a 17 | self.assertEqual(1, foo(1, 1)) 18 | self.assertIsNone(foo(2, 2)) 19 | 20 | def test_function(self): 21 | def _condition(a): 22 | return a == 1 23 | @Conditional(condition=_condition) 24 | def foo(a, b): 25 | return a 26 | self.assertEqual(1, foo(1, 1)) 27 | self.assertIsNone(foo(2, 2)) 28 | 29 | def test_sub_class(self): 30 | class TestConditional(Conditional): 31 | def _test(self, a): 32 | return a == 1 33 | @TestConditional 34 | def foo(a, b): 35 | return a 36 | self.assertEqual(1, foo(1, 1)) 37 | self.assertIsNone(foo(2, 2)) 38 | 39 | class MethodTest(TestCase): 40 | def test_normal(self): 41 | class Foo(object): 42 | @Conditional(condition=lambda a: a == 1) 43 | def bar(self, a): 44 | return a 45 | foo = Foo() 46 | self.assertEqual(1, foo.bar(1)) 47 | self.assertIsNone(foo.bar(0)) 48 | 49 | def test_self_in_string(self): 50 | class Foo(object): 51 | def __init__(self, condition): 52 | self._condition = condition 53 | 54 | @Conditional(condition='self_._condition') 55 | def bar(self, a): 56 | return a 57 | foo = Foo(True) 58 | self.assertEqual(1, foo.bar(1)) 59 | foo = Foo(False) 60 | self.assertIsNone(foo.bar(0)) 61 | 62 | def test_self_in_lambda(self): 63 | class Foo(object): 64 | def __init__(self, condition): 65 | self._condition = condition 66 | 67 | @Conditional(condition=lambda self_: self_._condition) 68 | def bar(self, a): 69 | return a 70 | foo = Foo(True) 71 | self.assertEqual(1, foo.bar(1)) 72 | foo = Foo(False) 73 | self.assertIsNone(foo.bar(0)) 74 | 75 | def test_self_in_function(self): 76 | def _condition(self_): 77 | return self_._condition 78 | class Foo(object): 79 | def __init__(self, condition): 80 | self._condition = condition 81 | 82 | @Conditional(condition=_condition) 83 | def bar(self, a): 84 | return a 85 | foo = Foo(True) 86 | self.assertEqual(1, foo.bar(1)) 87 | foo = Foo(False) 88 | self.assertIsNone(foo.bar(0)) 89 | 90 | def test_self_in_sub_class(self): 91 | class TestConditional(Conditional): 92 | def _test(self, self_): 93 | return self_._condition 94 | class Foo(object): 95 | def __init__(self, condition): 96 | self._condition = condition 97 | 98 | @TestConditional 99 | def bar(self, a): 100 | return a 101 | foo = Foo(True) 102 | self.assertEqual(1, foo.bar(1)) 103 | foo = Foo(False) 104 | self.assertIsNone(foo.bar(0)) 105 | -------------------------------------------------------------------------------- /src/decorated/base/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import doctest 3 | import logging 4 | 5 | import six 6 | 7 | from decorated.base.dict import Dict 8 | from decorated.base.proxy import Proxy 9 | from decorated.base.thread_local import ThreadLocal 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | class ContextMeta(type): 14 | def __new__(mcs, name, bases, attrs): 15 | cls = type.__new__(mcs, name, bases, attrs) 16 | sub_class_enter = getattr(cls, '__enter__', None) 17 | def _enter(instance): 18 | result = instance 19 | if sub_class_enter is not None: 20 | result = sub_class_enter(instance) 21 | cls._current.set(instance) # pylint: disable=protected-access 22 | return result 23 | cls.__enter__ = _enter 24 | sub_class_exit = getattr(cls, '__exit__', None) 25 | def _exit(instance, *args, **kw): 26 | cls._current.set(None) # pylint: disable=protected-access 27 | if sub_class_exit is not None: 28 | sub_class_exit(instance, *args, **kw) 29 | cls.__exit__ = _exit 30 | cls._current = ThreadLocal() # pylint: disable=protected-access 31 | return cls 32 | 33 | def current(cls): 34 | return cls._current.get() 35 | 36 | class Context(six.with_metaclass(ContextMeta, Dict)): 37 | def __init__(self, **kw): 38 | super(Context, self).__init__(**kw) 39 | self._defers = None 40 | self._parent = None 41 | 42 | def __contains__(self, name): 43 | raise NotImplementedError() 44 | 45 | def __enter__(self): 46 | self._parent = Context.current() 47 | self._defers = [] 48 | return self 49 | 50 | def __exit__(self, error_type, error_value, traceback): 51 | for defer in self._defers: 52 | try: 53 | defer() 54 | except Exception: 55 | log.warn('Failed to execute defer "%s".', defer) 56 | Context._current.set(self._parent) 57 | 58 | def __getattr__(self, name): 59 | try: 60 | return super(Context, self).__getattr__(name) 61 | except AttributeError: 62 | if self._parent is None: 63 | raise 64 | else: 65 | try: 66 | return getattr(self._parent, name) 67 | except AttributeError: 68 | raise 69 | 70 | def __getitem__(self, name): 71 | raise NotImplementedError() 72 | 73 | def defer(self, action): 74 | self._defers.append(action) 75 | 76 | def dict(self): 77 | data = self._parent.dict() if self._parent else Dict() 78 | data.update({k: v for k, v in self.items() if not k.startswith('_')}) 79 | return data 80 | 81 | def get(self, name, default=None): 82 | raise NotImplementedError() 83 | 84 | class ContextProxy(Proxy): 85 | def __init__(self): 86 | super(ContextProxy, self).__init__() 87 | self._inited = True 88 | 89 | def __setattr__(self, name, value): 90 | if self.__dict__.get('_inited'): 91 | return setattr(self.target, name, value) 92 | else: 93 | self.__dict__[name] = value 94 | 95 | @property 96 | def target(self): 97 | context = Context._current.get() # pylint: disable=protected-access 98 | if context is None: 99 | raise ContextError('Context should be set first.') 100 | return context 101 | 102 | def get(self): 103 | return self.target 104 | 105 | class ContextError(Exception): 106 | pass 107 | 108 | ctx = ContextProxy() 109 | 110 | if __name__ == '__main__': 111 | doctest.testmod() 112 | -------------------------------------------------------------------------------- /test/base_test/function_test/method_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | from decorated.base.function import Function 3 | from unittest.case import TestCase 4 | import six 5 | 6 | class MethodTest(TestCase): 7 | def test_single_level(self): 8 | # set up 9 | class Foo(object): 10 | @Function 11 | def bar(self, a, b=0): 12 | return a + b 13 | 14 | # test by class 15 | self.assertEqual(Foo, Foo.bar.im_class) 16 | self.assertIsNone(Foo.bar.im_self) 17 | self.assertIsNone(Foo.bar.__self__) 18 | self.assertEqual(('self', 'a', 'b'), Foo.bar.im_func.params) 19 | self.assertEqual(('self', 'a', 'b'), Foo.bar.__func__.params) 20 | self.assertEquals(3, Foo.bar(Foo(), 1, b=2)) 21 | 22 | # test by instance 23 | foo = Foo() 24 | self.assertEqual(Foo, foo.bar.im_class) 25 | self.assertEqual(foo, foo.bar.im_self) 26 | self.assertEqual(foo, foo.bar.__self__) 27 | self.assertEqual(('self', 'a', 'b'), foo.bar.im_func.params) 28 | self.assertEqual(('self', 'a', 'b'), foo.bar.__func__.params) 29 | self.assertEquals(3, foo.bar(1, b=2)) 30 | 31 | def test_multi_levels(self): 32 | # set up 33 | class Foo(object): 34 | @Function 35 | @Function 36 | def bar(self, a, b=0): 37 | return a + b 38 | 39 | # test by class 40 | self.assertEqual(Foo, Foo.bar.im_class) 41 | self.assertIsNone(Foo.bar.im_self) 42 | self.assertIsNone(Foo.bar.__self__) 43 | self.assertEqual(('self', 'a', 'b'), Foo.bar.im_func.params) 44 | self.assertEqual(('self', 'a', 'b'), Foo.bar.__func__.params) 45 | self.assertEquals(3, Foo.bar(Foo(), 1, b=2)) 46 | 47 | # test by instance 48 | foo = Foo() 49 | self.assertEqual(Foo, foo.bar.im_class) 50 | self.assertEqual(foo, foo.bar.im_self) 51 | self.assertEqual(foo, foo.bar.__self__) 52 | self.assertEqual(('self', 'a', 'b'), foo.bar.im_func.params) 53 | self.assertEqual(('self', 'a', 'b'), foo.bar.__func__.params) 54 | self.assertEquals(3, foo.bar(1, b=2)) 55 | 56 | def test_non_call(self): 57 | # set up 58 | class TestFunction(Function): 59 | def bar2(self, foo): 60 | MethodTest.foo = foo 61 | return 2 62 | class Foo(object): 63 | @TestFunction 64 | def bar(self, a, b=0): 65 | return a + b 66 | 67 | # test 68 | foo = Foo() 69 | self.assertEqual(2, foo.bar.bar2()) 70 | self.assertIsInstance(self.foo, Foo) 71 | 72 | def test_static_method(self): 73 | # set up 74 | class Foo(object): 75 | @staticmethod 76 | @Function 77 | def bar(a, b=0): 78 | return a + b 79 | 80 | # test 81 | self.assertEqual(('a', 'b'), Foo.bar.params) 82 | self.assertEquals(3, Foo.bar(1, b=2)) 83 | 84 | def test_class_method(self): 85 | # set up 86 | class Foo(object): 87 | @classmethod 88 | @Function 89 | def bar(cls, a, b=0): 90 | return a + b 91 | 92 | # test 93 | if six.PY2: 94 | self.assertEqual(type, Foo.bar.im_class) 95 | self.assertEqual(Foo, Foo.bar.im_self) 96 | self.assertEqual(('cls', 'a', 'b'), Foo.bar.im_func.params) 97 | elif six.PY3: 98 | self.assertEqual(Foo, Foo.bar.__self__) 99 | self.assertEqual(('cls', 'a', 'b'), Foo.bar.__func__.params) 100 | else: 101 | self.fail() 102 | self.assertEquals(3, Foo.bar(1, b=2)) 103 | -------------------------------------------------------------------------------- /test/decorators_test/cache_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.base.context import Context, ctx 3 | from decorated.decorators.cache import SimpleCache, LruCache 4 | from unittest.case import TestCase 5 | 6 | class SimpleCacheTest(TestCase): 7 | def test_get_set(self): 8 | cache = SimpleCache() 9 | self.assertIsNone(cache.get('a')) 10 | cache.set('a', 1) 11 | self.assertEquals(1, cache.get('a')) 12 | cache.delete('a') 13 | self.assertIsNone(cache.get('a')) 14 | 15 | def test_clear(self): 16 | cache = SimpleCache() 17 | cache.set('a', 1) 18 | cache.clear() 19 | self.assertIsNone(cache.get('a')) 20 | 21 | class LruCacheTest(TestCase): 22 | def test_get_set(self): 23 | cache = LruCache() 24 | self.assertIsNone(cache.get('a')) 25 | cache.set('a', 1) 26 | self.assertEquals(1, cache.get('a')) 27 | cache.delete('a') 28 | self.assertIsNone(cache.get('a')) 29 | 30 | def test_clear(self): 31 | cache = LruCache() 32 | cache.set('a', 1) 33 | cache.clear() 34 | self.assertIsNone(cache.get('a')) 35 | 36 | class CacheTest(TestCase): 37 | def setUp(self): 38 | super(CacheTest, self).setUp() 39 | self.cache = SimpleCache() 40 | self.calls = 0 41 | 42 | def test_simple(self): 43 | # set up 44 | @self.cache.cache('/{id}') 45 | def foo(id): 46 | self.calls += 1 47 | return id 48 | @self.cache.uncache('/{id}') 49 | def unfoo(id): 50 | return id 51 | 52 | # test 53 | self.assertEqual(0, len(self.cache._data)) 54 | 55 | result = foo(1) 56 | self.assertEqual(1, result) 57 | self.assertEqual(1, self.calls) 58 | self.assertEqual(1, len(self.cache._data)) 59 | 60 | self.assertEqual(1, foo(1)) 61 | self.assertEqual(1, self.calls) 62 | self.assertEqual(1, len(self.cache._data)) 63 | 64 | result = unfoo(1) 65 | self.assertEqual(1, result) 66 | self.assertEqual(0, len(self.cache._data)) 67 | 68 | self.assertEqual(1, foo(1)) 69 | self.assertEqual(2, self.calls) 70 | self.assertEqual(1, len(self.cache._data)) 71 | 72 | self.assertEqual(2, foo(2)) 73 | self.assertEqual(3, self.calls) 74 | self.assertEqual(2, len(self.cache._data)) 75 | 76 | def test_ttl(self): 77 | # set up 78 | @self.cache.cache('/{id}', ttl=-1) 79 | def foo(id): 80 | self.calls += 1 81 | return id 82 | 83 | # test 84 | foo(1) 85 | self.assertEqual(1, self.calls) 86 | foo(1) 87 | self.assertEqual(2, self.calls) 88 | 89 | def test_extra_vars(self): 90 | # set up 91 | @self.cache.cache('/{a}/{ctx.b}', vars={'a': 1, 'ctx': ctx}) 92 | def foo(): 93 | pass 94 | @self.cache.uncache('/{a}/{ctx.b}', vars={'a': 1, 'ctx': ctx}) 95 | def unfoo(): 96 | pass 97 | with Context(b=2): 98 | foo() 99 | self.assertIn('/1/2', self.cache._data) 100 | unfoo() 101 | self.assertNotIn('/1/2', self.cache._data) 102 | 103 | def test_invalidate(self): 104 | # set up 105 | foo_cache = self.cache.cache('/{id}') 106 | @foo_cache 107 | def foo(id): 108 | pass 109 | @foo_cache.invalidate 110 | def unfoo(id): 111 | pass 112 | 113 | # test 114 | self.assertEqual(0, len(self.cache._data)) 115 | 116 | foo(1) 117 | self.assertEqual(1, len(self.cache._data)) 118 | 119 | unfoo(1) 120 | self.assertEqual(0, len(self.cache._data)) 121 | -------------------------------------------------------------------------------- /test/base_test/function_test/function_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import inspect 3 | from unittest.case import TestCase 4 | 5 | from decorated.base.function import ArgError, Function 6 | 7 | 8 | @Function 9 | def foo(a, b=0): 10 | return a + b 11 | 12 | class InitTest(TestCase): 13 | def test_no_arg(self): 14 | class _decorator(Function): 15 | def _init(self): 16 | self.inited = True 17 | @_decorator 18 | def foo(): 19 | pass 20 | self.assertTrue(foo.inited) 21 | 22 | def test_with_args(self): 23 | class _decorator(Function): 24 | def _init(self, a, b=0): 25 | self.a = a 26 | self.b = b 27 | @_decorator(1, b=2) 28 | def foo(): 29 | pass 30 | self.assertEqual(1, foo.a) 31 | self.assertEqual(2, foo.b) 32 | 33 | class DecorateTest(TestCase): 34 | def test_no_init_args(self): 35 | @Function 36 | def foo(a, b=0): 37 | pass 38 | self.assertEquals('foo', foo.__name__) 39 | self.assertTrue(hasattr(foo, 'func_code') or hasattr(foo, '__code__')) 40 | self.assertEquals(('a', 'b'), foo.params) 41 | self.assertEquals(('a',), foo.required_params) 42 | self.assertEquals((('b',0),), foo.optional_params) 43 | self.assertEqual(foo._call, foo._decorate_or_call) 44 | 45 | def test_with_init_args(self): 46 | @Function(1) 47 | def foo(a, b=0): 48 | pass 49 | self.assertEquals('foo', foo.__name__) 50 | self.assertTrue(hasattr(foo, 'func_code') or hasattr(foo, '__code__')) 51 | self.assertEquals(('a', 'b'), foo.params) 52 | self.assertEquals(('a',), foo.required_params) 53 | self.assertEquals((('b',0),), foo.optional_params) 54 | self.assertEqual(foo._call, foo._decorate_or_call) 55 | 56 | def test_multi_levels(self): 57 | @Function 58 | @Function 59 | def foo(a, b=0): 60 | pass 61 | self.assertEquals('foo', foo.__name__) 62 | self.assertTrue(hasattr(foo, 'func_code') or hasattr(foo, '__code__')) 63 | self.assertEquals(('a', 'b'), foo.params) 64 | self.assertEquals(('a',), foo.required_params) 65 | self.assertEquals((('b',0),), foo.optional_params) 66 | 67 | class FuncTest(TestCase): 68 | def test_raw_function(self): 69 | @Function 70 | def foo(): 71 | pass 72 | self.assertTrue(inspect.isfunction(foo.func)) 73 | 74 | def test_function_wrapper(self): 75 | @Function 76 | @Function 77 | def foo(): 78 | pass 79 | self.assertTrue(inspect.isfunction(foo.func)) 80 | 81 | class StrTest(TestCase): 82 | def test(self): 83 | s = str(foo) 84 | self.assertEquals('', s) 85 | 86 | class ResolveArgsTest(TestCase): 87 | def test_simple(self): 88 | d = foo._resolve_args(1, b=2) 89 | self.assertEquals(2, len(d)) 90 | self.assertEquals(1, d['a']) 91 | self.assertEquals(2, d['b']) 92 | 93 | def test_default_arg(self): 94 | d = foo._resolve_args(1) 95 | self.assertEquals(2, len(d)) 96 | self.assertEquals(1, d['a']) 97 | self.assertEquals(0, d['b']) 98 | 99 | def test_kw_as_args(self): 100 | d = foo._resolve_args(1, 2) 101 | self.assertEquals(2, len(d)) 102 | self.assertEquals(1, d['a']) 103 | self.assertEquals(2, d['b']) 104 | 105 | def test_arg_as_kw(self): 106 | d = foo._resolve_args(a=1, b=2) 107 | self.assertEquals(2, len(d)) 108 | self.assertEquals(1, d['a']) 109 | self.assertEquals(2, d['b']) 110 | 111 | def test_missing_arg(self): 112 | with self.assertRaises(ArgError): 113 | foo._resolve_args() 114 | 115 | class CallTest(TestCase): 116 | def test_no_init_arg(self): 117 | result = foo(1, b=2) 118 | self.assertEqual(3, result) 119 | -------------------------------------------------------------------------------- /test/base_test/template_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import six 3 | 4 | from decorated.base import template 5 | from decorated.base.template import Template 6 | from testutil import DecoratedTest 7 | 8 | class GenerateSourceTest(DecoratedTest): 9 | def test_single_string(self): 10 | source = template._generate_source('aaa') 11 | self.assertMultiLineEqual(''' 12 | parts = [] 13 | parts.append(u'aaa') 14 | result = ''.join(parts) 15 | '''.strip(), source) 16 | 17 | def test_single_variable(self): 18 | source = template._generate_source('{a}') 19 | expected = ''' 20 | parts = [] 21 | try: 22 | parts.append(%s(a)) 23 | except Exception: 24 | parts.append(u'{error:a}') 25 | result = ''.join(parts) 26 | ''' % template._DEFAULT_STRING_TYPE 27 | self.assertMultiLineEqual(expected.strip(), source) 28 | 29 | def test_multi_variables(self): 30 | source = template._generate_source('{a}{b}') 31 | expected = ''' 32 | parts = [] 33 | try: 34 | parts.append(%s(a)) 35 | except Exception: 36 | parts.append(u'{error:a}') 37 | try: 38 | parts.append(%s(b)) 39 | except Exception: 40 | parts.append(u'{error:b}') 41 | result = ''.join(parts) 42 | ''' % (template._DEFAULT_STRING_TYPE, template._DEFAULT_STRING_TYPE) 43 | self.assertMultiLineEqual(expected.strip(), source) 44 | 45 | def test_strings_and_variables(self): 46 | source = template._generate_source('aaa {a} bbb {b} ccc') 47 | expected = ''' 48 | parts = [] 49 | parts.append(u'aaa ') 50 | try: 51 | parts.append(%s(a)) 52 | except Exception: 53 | parts.append(u'{error:a}') 54 | parts.append(u' bbb ') 55 | try: 56 | parts.append(%s(b)) 57 | except Exception: 58 | parts.append(u'{error:b}') 59 | parts.append(u' ccc') 60 | result = ''.join(parts) 61 | ''' % (template._DEFAULT_STRING_TYPE, template._DEFAULT_STRING_TYPE) 62 | self.assertMultiLineEqual(expected.strip(), source) 63 | 64 | def test_unicode(self): 65 | source = template._generate_source(u'一{u"二"}三') 66 | expected = u''' 67 | parts = [] 68 | parts.append(u'一') 69 | try: 70 | parts.append(%s(u"二")) 71 | except Exception: 72 | parts.append(u'{error:u"二"}') 73 | parts.append(u'三') 74 | result = ''.join(parts) 75 | ''' % template._DEFAULT_STRING_TYPE 76 | self.assertMultiLineEqual(expected.strip(), source) 77 | 78 | def test_with_methods(self): 79 | source = template._generate_source('aaa {a.lower().capitalize()} bbb') 80 | expected = ''' 81 | parts = [] 82 | parts.append(u'aaa ') 83 | try: 84 | parts.append(%s(a.lower().capitalize())) 85 | except Exception: 86 | parts.append(u'{error:a.lower().capitalize()}') 87 | parts.append(u' bbb') 88 | result = ''.join(parts) 89 | ''' % template._DEFAULT_STRING_TYPE 90 | self.assertMultiLineEqual(expected.strip(), source) 91 | 92 | def test_expression(self): 93 | source = template._generate_source('aaa {a + b} bbb') 94 | expected = ''' 95 | parts = [] 96 | parts.append(u'aaa ') 97 | try: 98 | parts.append(%s(a + b)) 99 | except Exception: 100 | parts.append(u'{error:a + b}') 101 | parts.append(u' bbb') 102 | result = ''.join(parts) 103 | ''' % template._DEFAULT_STRING_TYPE 104 | self.assertMultiLineEqual(expected.strip(), source) 105 | 106 | def test_escaping(self): 107 | source = template._generate_source('"\'\n') 108 | self.assertMultiLineEqual(''' 109 | parts = [] 110 | parts.append(u'"\\'\\n') 111 | result = ''.join(parts) 112 | '''.strip(), source) 113 | 114 | class TemplateTest(DecoratedTest): 115 | def test_success(self): 116 | template = Template('aaa {a + b} bbb') 117 | result = template(a=1, b=2) 118 | self.assertEqual('aaa 3 bbb', result) 119 | 120 | def test_unicode(self): 121 | template = Template(u'一{u"二"}三') 122 | result = template(a=1, b=2) 123 | self.assertEqual(u'一二三', result) 124 | 125 | def test_failed(self): 126 | template = Template('aaa {a + c} bbb') 127 | result = template(a=1, b=2) 128 | self.assertEqual('aaa {error:a + c} bbb', result) 129 | 130 | def test_bad_syntax(self): 131 | with self.assertRaises(Exception): 132 | Template('aaa {!@#$%} bbb') 133 | -------------------------------------------------------------------------------- /src/decorated/decorators/validations/validators/number.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decimal import Decimal 3 | 4 | import six 5 | 6 | from decorated.decorators.validations.validators.misc import TypeValidator 7 | 8 | _NUMBER_TYPES = list(six.integer_types) + [float, Decimal] 9 | try: 10 | from cdecimal import Decimal as CDecimal 11 | _NUMBER_TYPES.append(CDecimal) 12 | except Exception: 13 | pass 14 | _NUMBER_TYPES = tuple(_NUMBER_TYPES) 15 | 16 | class NumberValidator(TypeValidator): 17 | def __init__(self, param, error_class=None): 18 | super(NumberValidator, self).__init__(param, _NUMBER_TYPES, error_class=error_class) 19 | 20 | class BetweenValidator(NumberValidator): 21 | def __init__(self, param, lower, upper, error_class=None): 22 | super(BetweenValidator, self).__init__(param, error_class=error_class) 23 | self._lower = lower 24 | self._upper = upper 25 | 26 | def _validate(self, value): 27 | ''' 28 | >>> BetweenValidator('score', 1, 10)._validate(5) 29 | >>> BetweenValidator('score', 1, 10)._validate(0) 30 | 'should be between 1 and 10' 31 | >>> BetweenValidator('score', 1, 10)._validate(11) 32 | 'should be between 1 and 10' 33 | ''' 34 | if value < self._lower or value > self._upper: 35 | return 'should be between %s and %s' % (self._lower, self._upper) 36 | 37 | class GeValidator(NumberValidator): 38 | def __init__(self, param, threshold, error_class=None): 39 | super(GeValidator, self).__init__(param, error_class=error_class) 40 | self._threshold = threshold 41 | 42 | def _validate(self, value): 43 | ''' 44 | >>> GeValidator('value', 0)._validate(1) 45 | >>> GeValidator('value', 0)._validate(0) 46 | >>> GeValidator('value', 0)._validate(-1) 47 | 'should be >=0' 48 | ''' 49 | if value < self._threshold: 50 | return 'should be >=%s' % self._threshold 51 | 52 | class GtValidator(NumberValidator): 53 | def __init__(self, param, threshold, error_class=None): 54 | super(GtValidator, self).__init__(param, error_class=error_class) 55 | self._threshold = threshold 56 | 57 | def _validate(self, value): 58 | ''' 59 | >>> GtValidator('value', 0)._validate(1) 60 | >>> GtValidator('value', 0)._validate(0) 61 | 'should be >0' 62 | >>> GtValidator('value', 0)._validate(-1) 63 | 'should be >0' 64 | ''' 65 | if value <= self._threshold: 66 | return 'should be >%s' % self._threshold 67 | 68 | class LeValidator(NumberValidator): 69 | def __init__(self, param, threshold, error_class=None): 70 | super(LeValidator, self).__init__(param, error_class=error_class) 71 | self._threshold = threshold 72 | 73 | def _validate(self, value): 74 | ''' 75 | >>> LeValidator('value', 0)._validate(-1) 76 | >>> LeValidator('value', 0)._validate(0) 77 | >>> LeValidator('value', 0)._validate(1) 78 | 'should be <=0' 79 | ''' 80 | if value > self._threshold: 81 | return 'should be <=%s' % self._threshold 82 | 83 | class LtValidator(NumberValidator): 84 | def __init__(self, param, threshold, error_class=None): 85 | super(LtValidator, self).__init__(param, error_class=error_class) 86 | self._threshold = threshold 87 | 88 | def _validate(self, value): 89 | ''' 90 | >>> LtValidator('value', 0)._validate(-1) 91 | >>> LtValidator('value', 0)._validate(0) 92 | 'should be <0' 93 | >>> LtValidator('value', 0)._validate(1) 94 | 'should be <0' 95 | ''' 96 | if value >= self._threshold: 97 | return 'should be <%s' % self._threshold 98 | 99 | class PositiveValidator(NumberValidator): 100 | def _validate(self, value): 101 | ''' 102 | >>> PositiveValidator('score')._validate(5) 103 | >>> PositiveValidator('score')._validate(-1) 104 | 'should be positive' 105 | ''' 106 | if value < 0: 107 | return 'should be positive' 108 | 109 | class NonNegativeValidator(NumberValidator): 110 | def _validate(self, value): 111 | ''' 112 | >>> NonNegativeValidator('score')._validate(5) 113 | >>> NonNegativeValidator('score')._validate(0) 114 | 'should be non negative' 115 | ''' 116 | if value <= 0: 117 | return 'should be non negative' 118 | -------------------------------------------------------------------------------- /src/decorated/decorators/events.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import doctest 3 | 4 | from six import with_metaclass 5 | 6 | from decorated.base.false import NOTSET 7 | from decorated.base.function import WrapperFunction 8 | from decorated.decorators.once import Once 9 | from decorated.decorators.remove_extra_args import RemoveExtraArgs 10 | from decorated.util import modutil 11 | 12 | 13 | class EventError(Exception): 14 | pass 15 | 16 | class EventListener(RemoveExtraArgs): 17 | def _decorate(self, func): 18 | super(EventListener, self)._decorate(func) 19 | self._validate() 20 | self._register() 21 | return self 22 | 23 | def _register(self): 24 | raise NotImplementedError() 25 | 26 | def _validate(self): 27 | available_fields = self._get_available_fields() 28 | for param in self.params: 29 | if param not in available_fields: 30 | raise EventError('Event "%s" does not have field "%s".' % (self._event_class.name, param)) 31 | 32 | class AfterEventListener(EventListener): 33 | def _get_available_fields(self): 34 | return self._event_class.fields + self._event_class.after_fields 35 | 36 | def _register(self): 37 | self._event_class._after_listeners.append(self) # pylint: disable=protected-access 38 | 39 | class BeforeEventListener(EventListener): 40 | def _get_available_fields(self): 41 | return self._event_class.fields 42 | 43 | def _register(self): 44 | self._event_class._before_listeners.append(self) # pylint: disable=protected-access 45 | 46 | class EventMetaType(type): 47 | def __init__(cls, name, bases, attrs): 48 | super(EventMetaType, cls).__init__(name, bases, attrs) 49 | cls._sources = [] 50 | cls._after_listeners = [] 51 | cls._before_listeners = [] 52 | 53 | @property 54 | def after(cls): 55 | class _EventListener(AfterEventListener): 56 | _event_class = cls 57 | 58 | return _EventListener 59 | 60 | @property 61 | def before(cls): 62 | class _EventListener(BeforeEventListener): 63 | _event_class = cls 64 | 65 | return _EventListener 66 | 67 | @property 68 | def name(cls): 69 | return _get_full_name(cls) 70 | 71 | 72 | class Event(with_metaclass(EventMetaType, WrapperFunction)): 73 | fields = () 74 | after_fields = () 75 | 76 | @classmethod 77 | def fire(cls, data=None): 78 | data = data or {} 79 | cls._execute_before_listeners(data) 80 | cls._execute_after_listeners(data) 81 | 82 | @classmethod 83 | def _execute_after_listeners(cls, data): 84 | for listener in cls._after_listeners: 85 | listener(**data) 86 | 87 | @classmethod 88 | def _execute_before_listeners(cls, data): 89 | for listener in cls._before_listeners: 90 | listener(**data) 91 | 92 | def _after(self, ret, *args, **kw): 93 | data = self._get_field_values(ret, *args, **kw) 94 | self._execute_after_listeners(data) 95 | 96 | def _before(self, *args, **kw): 97 | data = self._get_field_values(NOTSET, *args, **kw) 98 | self._execute_before_listeners(data) 99 | 100 | def _decorate(self, func): 101 | super(Event, self)._decorate(func) 102 | self._validate() 103 | type(self)._sources.append(self) 104 | return self 105 | 106 | def _get_field_values(self, ret, *args, **kw): 107 | args = self._resolve_args(*args, **kw) 108 | values = {k: args[k] for k in self._basic_fields} 109 | for key, expression in self._complex_fields.items(): 110 | values[key] = eval(expression, args) # pylint: disable=eval-used 111 | if ret != NOTSET: 112 | args['ret'] = ret 113 | for key, expression in self._after_complex_fields.items(): 114 | values[key] = eval(expression, args) # pylint: disable=eval-used 115 | return values 116 | 117 | def _init(self, **complex_fields): # pylint: disable=arguments-differ 118 | super(Event, self)._init() 119 | self._complex_fields = {k: v for k, v in complex_fields.items() if k in self.fields} 120 | self._after_complex_fields = {k: v for k, v in complex_fields.items() if k in self.after_fields} 121 | self._basic_fields = [f for f in self.fields if f not in complex_fields] 122 | 123 | def _validate(self): 124 | for field in self._basic_fields: 125 | if field not in self.params: 126 | raise EventError('Missing field "%s" in "%s".' % (field, type(self).name)) 127 | 128 | def event(fields, after_fields=()): 129 | fields_ = fields 130 | after_fields_ = after_fields 131 | class _Event(Event): 132 | fields = fields_ 133 | after_fields = after_fields_ 134 | return _Event 135 | 136 | @Once 137 | def init(*packages): 138 | for package in packages: 139 | modutil.load_tree(package) 140 | 141 | def _get_full_name(func): 142 | ''' 143 | >>> from decorated.util import modutil 144 | >>> _get_full_name(modutil.load_tree) 145 | 'decorated.util.modutil.load_tree' 146 | ''' 147 | return '%s.%s' % (func.__module__, func.__name__) 148 | 149 | if __name__ == '__main__': 150 | doctest.testmod() 151 | -------------------------------------------------------------------------------- /test/decorators_test/events_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from decorated.decorators.events import Event, EventError, event 3 | from fixtures2 import TestCase 4 | 5 | class FooEvent(Event): 6 | fields = ('a', 'b') 7 | ret_field = 'z' 8 | 9 | class ValidateTest(TestCase): 10 | def test_basic_fields(self): 11 | class TestEvent(Event): 12 | fields = ('a', 'b') 13 | @TestEvent 14 | def foo(a, b, c): 15 | pass 16 | 17 | def test_complex_fields(self): 18 | class TestEvent(Event): 19 | fields = ('a', 'b', 'c') 20 | @TestEvent(c='a + b') 21 | def foo(a, b): 22 | pass 23 | 24 | def test_failed(self): 25 | class TestEvent(Event): 26 | fields = ('a', 'b') 27 | with self.assertRaises(EventError): 28 | @TestEvent 29 | def foo(a): 30 | pass 31 | 32 | class ListenerValidateTest(TestCase): 33 | def test_basic_fields(self): 34 | class TestEvent(Event): 35 | fields = ('a', 'b') 36 | @TestEvent.before 37 | def before(a): 38 | pass 39 | @TestEvent.after 40 | def after(a): 41 | pass 42 | 43 | def test_after_fields(self): 44 | class TestEvent(Event): 45 | fields = ('a', 'b') 46 | after_fields = ('c',) 47 | @TestEvent(c='ret') 48 | def foo(a, b): 49 | return a + b 50 | @TestEvent.before 51 | def before(a, b): 52 | pass 53 | @TestEvent.after 54 | def after(c): 55 | pass 56 | 57 | def test_failed(self): 58 | class TestEvent(Event): 59 | fields = ('a', 'b') 60 | with self.assertRaises(EventError): 61 | @TestEvent.before 62 | def before(a, b, c): 63 | pass 64 | with self.assertRaises(EventError): 65 | @TestEvent.after 66 | def after(a, b, c): 67 | pass 68 | 69 | class CallTest(TestCase): 70 | def test_basic(self): 71 | # set up 72 | calls = [] 73 | class TestEvent(Event): 74 | fields = ('a', 'b') 75 | @TestEvent 76 | def foo(a, b): 77 | return a + b 78 | @TestEvent.before 79 | def before(a, b): 80 | calls.append((a, b)) 81 | @TestEvent.after 82 | def after(a, b): 83 | calls.append((a, b)) 84 | 85 | # test 86 | result = foo(1, 2) 87 | self.assertEqual(3, result) 88 | self.assertEqual(2, len(calls)) 89 | self.assertEqual((1, 2), calls[0]) 90 | self.assertEqual((1, 2), calls[1]) 91 | 92 | def test_complex_field(self): 93 | # set up 94 | calls = [] 95 | class TestEvent(Event): 96 | fields = ('a', 'b', 'c') 97 | @TestEvent(c='a + b') 98 | def foo(a, b): 99 | return a + b 100 | @TestEvent.before 101 | def before(c): 102 | calls.append((c,)) 103 | @TestEvent.after 104 | def after(c): 105 | calls.append((c,)) 106 | 107 | # test 108 | foo(1, 2) 109 | self.assertEqual(2, len(calls)) 110 | self.assertEqual((3,), calls[0]) 111 | self.assertEqual((3,), calls[1]) 112 | 113 | def test_after_fields(self): 114 | # set up 115 | calls = [] 116 | class TestEvent(Event): 117 | fields = ('a', 'b') 118 | after_fields = ('c',) 119 | @TestEvent(c='ret') 120 | def foo(a, b): 121 | return a + b 122 | @TestEvent.before 123 | def before(a, b): 124 | calls.append((a, b)) 125 | @TestEvent.after 126 | def after(c): 127 | calls.append((c,)) 128 | 129 | # test 130 | foo(1, 2) 131 | self.assertEqual(2, len(calls)) 132 | self.assertEqual((1, 2), calls[0]) 133 | self.assertEqual((3,), calls[1]) 134 | 135 | class FireTest(TestCase): 136 | def test(self): 137 | # set up 138 | calls = [] 139 | class TestEvent(Event): 140 | fields = ('a', 'b') 141 | @TestEvent.before 142 | def before(a, b): 143 | calls.append((a, b)) 144 | @TestEvent.after 145 | def after(a, b): 146 | calls.append((a, b)) 147 | 148 | # test 149 | TestEvent.fire({'a': 1, 'b': 2}) 150 | self.assertEqual(2, len(calls)) 151 | self.assertEqual((1, 2), calls[0]) 152 | self.assertEqual((1, 2), calls[1]) 153 | 154 | class BuilderTest(TestCase): 155 | def test(self): 156 | # set up 157 | calls = [] 158 | test_event = event(('a', 'b')) 159 | @test_event 160 | def foo(a, b): 161 | return a + b 162 | @test_event.before 163 | def before(a, b): 164 | calls.append((a, b)) 165 | @test_event.after 166 | def after(a, b): 167 | calls.append((a, b)) 168 | 169 | # test 170 | result = foo(1, 2) 171 | self.assertEqual(3, result) 172 | self.assertEqual(2, len(calls)) 173 | self.assertEqual((1, 2), calls[0]) 174 | self.assertEqual((1, 2), calls[1]) 175 | -------------------------------------------------------------------------------- /src/decorated/base/function.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | import functools 3 | import importlib 4 | import inspect 5 | import os 6 | 7 | from decorated.base.proxy import NoTargetError, Proxy 8 | 9 | WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__', '__code__', 'func_code') 10 | 11 | 12 | class ArgError(Exception): 13 | def __init__(self, param, message): 14 | super(ArgError, self).__init__(message) 15 | self.param = param 16 | 17 | 18 | class Function(Proxy): 19 | enabled = os.getenv('DECORATED_ENABLED', '1') != '0' 20 | 21 | def __init__(self, *args, **kw): 22 | super(Function, self).__init__() 23 | self.params = None 24 | self.required_params = None 25 | self.optional_params = None 26 | self._func = None 27 | self._decorate_or_call = self._decorate 28 | if self._is_init_args(*args, **kw): 29 | self._init(*args, **kw) 30 | else: 31 | self._init() 32 | self._decorate(args[0]) 33 | 34 | def __call__(self, *args, **kw): 35 | if self.enabled: 36 | return self._decorate_or_call(*args, **kw) 37 | else: 38 | if self._decorate_or_call == self._decorate: 39 | return self._decorate(*args, **kw) 40 | else: 41 | return self._func(*args, **kw) 42 | 43 | def __get__(self, obj, cls): 44 | if obj is None: 45 | method = Function(self) 46 | else: 47 | method = partial(Function, call_args=(obj,))(self) 48 | method.im_class = cls 49 | method.im_func = method.__func__ = self 50 | method.im_self = method.__self__ = obj 51 | return method 52 | 53 | def __str__(self): 54 | return '' % (self._func.__module__, self.__name__) 55 | 56 | @property 57 | def func(self): 58 | return self._func.func if isinstance(self._func, Function) else self._func 59 | 60 | @property 61 | def target(self): 62 | if self._func is None: 63 | raise NoTargetError() 64 | return self._func 65 | 66 | def _call(self, *args, **kw): 67 | return self._func(*args, **kw) 68 | 69 | def _decorate(self, func): 70 | self._func = func 71 | functools.update_wrapper(self, func, WRAPPER_ASSIGNMENTS, updated=()) 72 | if isinstance(func, Function): 73 | self.params = func.params 74 | self.required_params = func.required_params 75 | self.optional_params = func.optional_params 76 | else: 77 | self._parse_params(func) 78 | 79 | # this is mainly for doctest purpose 80 | # doctest does not test on objects (Function instances) 81 | # so we put the original function back into the module 82 | mod = importlib.import_module(func.__module__) 83 | setattr(mod, '__original_%s' % func.__name__, func) 84 | self._decorate_or_call = self._call 85 | return self 86 | 87 | def _init(self, *args, **kw): 88 | pass 89 | 90 | def _is_init_args(self, *args, **kw): 91 | return len(args) != 1 or not callable(args[0]) or len(kw) != 0 92 | 93 | def _parse_params(self, func): 94 | self.params, _, _, defaults = inspect.getargspec(func) 95 | if defaults: 96 | self.required_params = self.params[:-len(defaults)] 97 | self.optional_params = [] 98 | for i in range(len(defaults) - 1, -1, -1): 99 | self.optional_params.append((self.params[-1 - i], defaults[-1 - i])) 100 | else: 101 | self.required_params = self.params 102 | self.optional_params = () 103 | if _is_bound_method(func): 104 | self.params = self.params[1:] 105 | self.required_params = self.required_params[1:] 106 | self.params = tuple(self.params) 107 | self.required_params = tuple(self.required_params) 108 | self.optional_params = tuple(self.optional_params) 109 | 110 | def _resolve_args(self, *args, **kw): 111 | results = {name: default for name, default in self.optional_params} 112 | for param, arg in zip(self.params, args): 113 | results[param] = arg 114 | results.update(kw) 115 | for name in self.params: 116 | if name not in results: 117 | msg = 'Missing argument "%s" for %s.' % (name, str(self)) 118 | raise ArgError(name, msg) 119 | results = {k: v for k, v in results.items() if k in self.params} 120 | return results 121 | 122 | 123 | class WrapperFunction(Function): 124 | def _call(self, *args, **kw): 125 | self._before(*args, **kw) 126 | try: 127 | result = super(WrapperFunction, self)._call(*args, **kw) 128 | except Exception as e: 129 | self._error(e, *args, **kw) 130 | raise 131 | else: 132 | self._after(result, *args, **kw) 133 | return result 134 | 135 | def _after(self, ret, *args, **kw): 136 | pass 137 | 138 | def _before(self, *args, **kw): 139 | pass 140 | 141 | def _error(self, error, *args, **kw): 142 | pass 143 | 144 | 145 | class ContextFunction(WrapperFunction): 146 | def __enter__(self): 147 | self._before() 148 | return self 149 | 150 | def __exit__(self, error_type, error_value, traceback): 151 | if error_value is None: 152 | self._after(None) 153 | else: 154 | self._error(error_value) 155 | 156 | 157 | def partial(func, init_args=(), init_kw=None, call_args=(), call_kw=None): 158 | if init_kw is None: 159 | init_kw = {} 160 | if call_kw is None: 161 | call_kw = {} 162 | class _PartialFunction(func): 163 | enabled = True 164 | 165 | def __getattr__(self, name): 166 | attr = getattr(self._func, name) 167 | if callable(attr): 168 | method = attr 169 | def _wrapper(*args, **kw): 170 | args = tuple(call_args) + args 171 | merged_kw = dict(call_kw) 172 | merged_kw.update(kw) 173 | return method(*args, **merged_kw) 174 | attr = _wrapper 175 | return attr 176 | 177 | def __str__(self): 178 | return '' % (self._func.__module__, self.__name__) 179 | 180 | def _init(self, *args, **kw): 181 | args = tuple(init_args) + args 182 | kw.update(init_kw) 183 | super(_PartialFunction, self)._init(*args, **kw) 184 | 185 | def _call(self, *args, **kw): 186 | args = tuple(call_args) + args 187 | merged_kw = dict(call_kw) 188 | merged_kw.update(kw) 189 | return super(_PartialFunction, self)._call(*args, **merged_kw) 190 | 191 | def _parse_params(self, func): 192 | super(_PartialFunction, self)._parse_params(func) 193 | self.params = self.params[len(call_args):] 194 | self.required_params = self.params[len(call_args):] 195 | self.params = tuple([p for p in self.params if p not in call_kw]) 196 | self.required_params = tuple([p for p in self.required_params if p not in call_kw]) 197 | self.optional_params = tuple([(k, v) for (k, v) in self.optional_params if k not in call_kw]) 198 | return _PartialFunction 199 | 200 | 201 | def _is_bound_method(func): 202 | ''' 203 | >>> def foo(): 204 | ... pass 205 | >>> _is_bound_method(foo) 206 | False 207 | 208 | >>> class Foo(object): 209 | ... def bar(self): 210 | ... pass 211 | >>> _is_bound_method(Foo.bar) 212 | False 213 | >>> _is_bound_method(Foo().bar) 214 | True 215 | ''' 216 | return hasattr(func, '__self__') and func.__self__ is not None 217 | --------------------------------------------------------------------------------