├── 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 |
--------------------------------------------------------------------------------