├── forwardable ├── test │ ├── __init__.py │ ├── test_def_delegators.py │ ├── test_forwardable.py │ └── test_def_delegator.py └── __init__.py ├── MANIFEST.in ├── .gitignore ├── tox.ini ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── setup.py └── README.rst /forwardable/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /MANIFEST 2 | /dist 3 | *.egg-info 4 | *.py[co] 5 | 6 | /.tox/ 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py33, pypy 3 | 4 | [testenv] 5 | commands = {envpython} setup.py test 6 | deps = 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | - 3.4 5 | - 3.5 6 | - 3.6 7 | - 3.7 8 | - pypy 9 | - pypy3 10 | 11 | script: ./setup.py test 12 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | * 0.4.1 2 | 3 | - Fix basestring isssue for Py3K 4 | 5 | * 0.4.0 6 | 7 | - ``def_delegators`` now accepts string form for 2nd argument. 8 | Just like ``collections.namedtuple`` does. 9 | 10 | * 0.3.1 11 | 12 | - Various minor fixes in README.rst 13 | 14 | * 0.3.0 15 | 16 | - Support attr plucking. 17 | 18 | * 0.2.1 19 | 20 | - Update changelog for 0.2.0 (duh) 21 | 22 | * 0.2.0 23 | 24 | - Support set/delattr 25 | - Docstrings for the module and its functions 26 | 27 | * 0.1.1 28 | 29 | - Include LICENSE and friends as package data 30 | 31 | * 0.1.0 32 | 33 | - Initial release 34 | -------------------------------------------------------------------------------- /forwardable/test/test_def_delegators.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from forwardable import def_delegators 4 | 5 | class TestDefDelegators(TestCase): 6 | def test_def_delegators(self): 7 | class Foo(object): 8 | def_delegators("dct", ["keys", "values"]) 9 | dct = {'key': 42} 10 | 11 | foo = Foo() 12 | 13 | self.assertEqual(list(foo.keys()), ['key']) 14 | self.assertEqual(list(foo.values()), [42]) 15 | 16 | def test_attr_splitting(self): 17 | class Foo(object): 18 | def_delegators("dct", "keys, values") 19 | dct = {'key': 42} 20 | 21 | foo = Foo() 22 | 23 | self.assertEqual(list(foo.keys()), ['key']) 24 | self.assertEqual(list(foo.values()), [42]) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Whyme Lyu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | from forwardable import __version__ 6 | 7 | setup( 8 | name='forwardable', 9 | version=__version__, 10 | description="Forwardable as in Ruby's stdlib", 11 | url='https://github.com/5long/forwardable', 12 | long_description=open("README.rst").read(), 13 | author="Whyme Lyu", 14 | author_email="callme5long@gmail.com", 15 | packages=[ 16 | 'forwardable', 17 | 'forwardable.test', 18 | ], 19 | package_data={'': ['LICENSE', 'README.rst', 'CHANGELOG.rst']}, 20 | include_package_data=True, 21 | license="MIT", 22 | test_suite="forwardable.test", 23 | classifiers=[ 24 | 'Development Status :: 3 - Alpha', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Topic :: Software Development :: Code Generators', 28 | 'Programming Language :: Python', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3.4', 31 | 'Programming Language :: Python :: 3.5', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Programming Language :: Python :: 3.7', 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /forwardable/test/test_forwardable.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from forwardable import (forwardable, NotCalledInModuleScope, 4 | WrongDecoratorSyntax) 5 | 6 | assert "def_delegator" not in locals() 7 | 8 | @forwardable() 9 | class Foo(object): 10 | assert "def_delegator" not in locals() 11 | def_delegator('dct', 'keys') 12 | def_delegators('dct', ['values', 'items']) 13 | dct = {'key': 42} 14 | 15 | @forwardable() 16 | class DocFoo(object): 17 | assert "def_delegator" not in locals() 18 | def_delegator('dct', 'keys', doc_from_class=dict) 19 | def_delegators('dct', ['values', 'items'], doc_from_class=dict) 20 | dct = {'key': 42} 21 | 22 | 23 | assert "def_delegator" not in locals() 24 | 25 | class TestForwardable(TestCase): 26 | def test_inject_def_delegator(self): 27 | foo = Foo() 28 | 29 | self.assertEqual(list(foo.keys()), ['key']) 30 | self.assertEqual(list(foo.values()), [42]) 31 | self.assertEqual(list(foo.items()), [('key', 42)]) 32 | 33 | self.assertFalse(hasattr(foo, "get")) 34 | 35 | def test_inject_with_doc(self): 36 | self.assertTrue(Foo.keys.__doc__ is None) 37 | 38 | foo = DocFoo() 39 | 40 | # Test normal functionality 41 | self.assertEqual(list(foo.keys()), ['key']) 42 | self.assertEqual(list(foo.values()), [42]) 43 | self.assertEqual(list(foo.items()), [('key', 42)]) 44 | 45 | self.assertFalse(hasattr(foo, "get")) 46 | 47 | # Check that docstrings of class is populated 48 | self.assertTrue(DocFoo.keys.__doc__ is not None) 49 | self.assertTrue(DocFoo.values.__doc__ is not None) 50 | self.assertTrue(DocFoo.items.__doc__ is not None) 51 | 52 | def test_in_non_module_scope(self): 53 | with self.assertRaises(NotCalledInModuleScope): 54 | @forwardable() 55 | class Foo(object): 56 | pass 57 | 58 | def test_wrong_decorator_syntax(self): 59 | with self.assertRaises(Exception): 60 | @forwardable 61 | class Foo(object): 62 | pass 63 | -------------------------------------------------------------------------------- /forwardable/test/test_def_delegator.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from forwardable import (def_delegator, def_delegators, 4 | NotCalledInClassScope) 5 | 6 | class Object(object): 7 | pass 8 | 9 | 10 | class TestForwardable(TestCase): 11 | def test_def_delegator(self): 12 | class Foo(object): 13 | def_delegator("dct", "keys") 14 | dct = {'key': 42} 15 | 16 | foo = Foo() 17 | 18 | self.assertTrue(hasattr(foo, "keys")) 19 | self.assertEqual(list(foo.keys()), ['key']) 20 | 21 | 22 | def test_called_in_non_class_scope(self): 23 | with self.assertRaises(NotCalledInClassScope): 24 | def_delegator("what", "ever") 25 | 26 | 27 | def test_delegating_special_method(self): 28 | class Foo(object): 29 | def_delegator("s", "__len__") 30 | def __init__(self): 31 | self.s = set() 32 | 33 | self.assertEqual(len(Foo()), 0) 34 | 35 | 36 | def test_property_deleting(self): 37 | class Foo(object): 38 | def_delegator("bar", "baz") 39 | 40 | foo = Foo() 41 | foo.bar = Object() 42 | foo.bar.baz = 42 43 | 44 | del foo.baz 45 | self.assertFalse(hasattr(foo, 'baz')) 46 | 47 | 48 | def test_hasattr(self): 49 | class Foo(object): 50 | def_delegator("bar", "baz") 51 | 52 | foo = Foo() 53 | foo.bar = Object() 54 | 55 | self.assertFalse(hasattr(foo, 'baz')) 56 | 57 | foo.bar.baz = 42 58 | self.assertTrue(hasattr(foo, 'baz')) 59 | 60 | 61 | def test_setattr(self): 62 | class Foo(object): 63 | def_delegator("bar", "baz") 64 | 65 | foo = Foo() 66 | foo.bar = Object() 67 | foo.baz = 42 68 | 69 | self.assertTrue(foo.bar.baz, 42) 70 | 71 | 72 | def test_attr_plucking(self): 73 | class Foo(object): 74 | def_delegator("bar.baz", "qoox") 75 | 76 | foo = Foo() 77 | foo.bar = Object() 78 | foo.bar.baz = Object() 79 | foo.bar.baz.qoox = 42 80 | 81 | self.assertEqual(foo.qoox, 42) 82 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Forwardable 2 | =========== 3 | 4 | Utility for easy object composition via delegation. Roughly ported from 5 | Ruby's forwardable_ standard library. 6 | 7 | Requirements 8 | ------------ 9 | 10 | Python 2.7 or 3.3 w/ standard library. Might work on other version of 11 | Python, too. 12 | 13 | Installation 14 | ------------ 15 | 16 | ``$ pip install forwardable`` 17 | 18 | Usage 19 | ----- 20 | 21 | Most Common Use Case 22 | ~~~~~~~~~~~~~~~~~~~~ 23 | 24 | The ``@forwardable.forwardable()`` decorator enables you to use 25 | ``def_delegator()`` and ``def_delegators()`` in a class definition block. 26 | 27 | Use ``def_delegators()`` to define multiple attr forwarding: 28 | 29 | .. code-block:: python 30 | 31 | from forwardable import forwardable 32 | 33 | @forwardable() # Note the () here, which is required. 34 | class Foo(object): 35 | def_delegators('bar', 'add, __len__') 36 | 37 | def __init__(self): 38 | self.bar = set() 39 | 40 | foo = Foo() 41 | foo.add(1) # Delegates to foo.bar.add() 42 | assert len(foo) == 1 # Magic methods works, too 43 | 44 | Easy, heh? 45 | 46 | Define a Single Forwarding 47 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 48 | 49 | In case you only need to delegate one method to a delegatee, just 50 | use ``def_delegator``: 51 | 52 | .. code-block:: python 53 | 54 | from forwardable import forwardable 55 | 56 | @forwardable() 57 | class Foo(object): 58 | def_delegator('bar', '__len__') 59 | 60 | def __init__(self): 61 | self.bar = set() 62 | 63 | assert len(Foo()) == 0 64 | 65 | And it should work just fine. Actually, ``def_delegators()`` calls 66 | ``def_delegator()`` under the hood. 67 | 68 | Plucking 69 | ~~~~~~~~ 70 | 71 | .. code-block:: python 72 | 73 | from forwardable import forwardable 74 | 75 | @forwardable() 76 | class MyDict(object): 77 | def_delegator('dct.get', '__call__') 78 | def __init__(self): 79 | self.dct = {'foo', 42} 80 | 81 | d = MyDict() 82 | # Equivlant to d.dct.get('foo') 83 | assert d('foo') == 42 84 | 85 | Less Magical Usage 86 | ~~~~~~~~~~~~~~~~~~ 87 | 88 | The ``@forwardable()`` decorator injects ``def_delegator{,s}`` into the 89 | module scope temporarily, which is why you don't have to import them 90 | explicitly. This is admittedly magical but discourages the usage 91 | of ``import *``. And it's always nice to type less characters whenever 92 | unnecessary. 93 | 94 | If you hesitate to utilize this injection magic, just explicitly say 95 | ``from forwardable import def_delegator, def_delegators``, use them in 96 | a class definition and you'll be fine. 97 | 98 | Links 99 | ----- 100 | 101 | * Source Repository: https://github.com/5long/forwardable 102 | * Feedback: https://github.com/5long/forwardable/issues 103 | 104 | License 105 | ------- 106 | 107 | MIT license. 108 | 109 | .. _forwardable: http://ruby-doc.org/stdlib-2.0/libdoc/forwardable/rdoc/Forwardable.html 110 | -------------------------------------------------------------------------------- /forwardable/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Easy delegation definition. A quick example: 3 | 4 | from forwardable import forwardable 5 | 6 | @forwardable() # Note the () here, which is required. 7 | class Foo(object): 8 | def_delegators('bar', 'add, __len__') 9 | 10 | def __init__(self) 11 | self.bar = set() 12 | 13 | foo = Foo() 14 | foo.add(1) # Delegates to foo.bar.add() 15 | assert len(foo) == 1 16 | """ 17 | 18 | __version__ = '0.4.1' 19 | 20 | __all__ = ["forwardable", "def_delegator", "def_delegators"] 21 | 22 | try: 23 | basestring 24 | except NameError: 25 | basestring = (str, bytes) 26 | 27 | import sys 28 | from operator import attrgetter 29 | 30 | class NotCalledInModuleScope(Exception): pass 31 | class NotCalledInClassScope(Exception): pass 32 | class WrongDecoratorSyntax(Exception): pass 33 | 34 | def def_delegator(wrapped, attr_name, doc_from_class=None, _call_stack_depth=1): 35 | """ 36 | Define a property ``attr_name`` in the current class scope which 37 | forwards accessing of ``self.`` to property 38 | ``self..``. 39 | 40 | Must be called in a class scope. 41 | """ 42 | frame = sys._getframe(_call_stack_depth) 43 | 44 | if not looks_like_class_frame(frame): 45 | raise NotCalledInClassScope 46 | 47 | get_wrapped_obj = attrgetter(wrapped) 48 | 49 | def getter(self): 50 | return getattr(get_wrapped_obj(self), attr_name) 51 | 52 | def setter(self, value): 53 | return setattr(get_wrapped_obj(self), attr_name, value) 54 | 55 | def deleter(self): 56 | return delattr(get_wrapped_obj(self), attr_name) 57 | 58 | doc = doc_from_class.__dict__[attr_name].__doc__ if doc_from_class else None 59 | 60 | scope = frame.f_locals 61 | scope[attr_name] = property(getter, setter, deleter, doc) 62 | 63 | 64 | def def_delegators(wrapped, attrs, doc_from_class=None): 65 | """ 66 | Define multiple delegations for a single delegatee. Roughly equivalent 67 | to def_delegator() in a for-loop. 68 | 69 | The ``attrs`` argument can be an iterable of attribute names, or 70 | a comma-and-spaces separated string of attribute names. The following 71 | form works identically: 72 | 73 | def_delegators(wrapped, ('foo', 'bar')) # Tuple of attribute names 74 | def_delegators(wrapped, 'foo bar') # Separated by space 75 | def_delegators(wrapped, 'foo, bar') # With optional comma 76 | 77 | Must be called in a class scope. 78 | """ 79 | attrs = split_attrs(attrs) if isinstance(attrs, basestring) else attrs 80 | for a in attrs: 81 | def_delegator(wrapped, a, doc_from_class=doc_from_class, _call_stack_depth=2) 82 | 83 | 84 | CLS_SCOPE_KEYS = ("__module__",) 85 | def looks_like_class_frame(frame): 86 | return all(k in frame.f_locals for k in CLS_SCOPE_KEYS) 87 | 88 | def is_module_frame(frame): 89 | return frame.f_globals is frame.f_locals 90 | 91 | def split_attrs(attrs): 92 | return attrs.replace(',', ' ').split() 93 | 94 | def inject(frame): 95 | if not is_module_frame(frame): 96 | raise NotCalledInModuleScope() 97 | frame.f_locals.update( 98 | def_delegator=def_delegator, 99 | def_delegators=def_delegators) 100 | 101 | 102 | def cleanup(scope): 103 | scope.pop("def_delegator") 104 | scope.pop("def_delegators") 105 | 106 | 107 | def forwardable(): 108 | """ 109 | A class decorator which makes def_delegator() and def_delegators() 110 | available in the class scope. 111 | 112 | This decorator must be used in the form of `@forwardable()`, instead of 113 | `@forwardable`. And it must be called in a module scope (which should be 114 | the case for most common class definitions). 115 | """ 116 | frame = sys._getframe(1) 117 | inject(frame) 118 | 119 | def decorate(cls): 120 | cleanup(frame.f_locals) 121 | return cls 122 | 123 | return decorate 124 | --------------------------------------------------------------------------------