├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.py ├── test_whatever.py ├── tox.ini └── whatever.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 3.10 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: "3.10" 18 | - name: Lint 19 | run: | 20 | pip install flake8 21 | flake8 whatever.py 22 | flake8 --ignore=E231,E711,E20,E22,E701 test_whatever.py 23 | 24 | test: 25 | runs-on: ubuntu-18.04 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | python-version: ["2.7", "3.3", "3.4", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "pypy2", "pypy3"] 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Set up Python ${{ matrix.python-version }} 34 | uses: actions/setup-python@v2 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | - name: Install dependencies 38 | run: pip install --upgrade pip pytest 39 | - name: Run tests 40 | run: pytest -W error 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist 3 | *.egg-info 4 | build 5 | docs/_build 6 | .tags* 7 | .coverage 8 | .tox 9 | .cache 10 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.7 2 | - support Python 3.11 3 | - include CHANGELOG into a release (Igor Gnatenko) 4 | 5 | 0.6 6 | - support Python 3.8 (Felix Yan) 7 | - explicit support for Python 3.7 8 | Backward incompatible changes: 9 | - dropped Python 2.6 and 3.3 support 10 | 11 | 0.5 12 | - added caller functionality to whatever 13 | - improved __code__ introspection 14 | - optimized things a bit 15 | 16 | 0.4.3 17 | - explicit support for Python 3.6 18 | - minor optimizations 19 | 20 | 0.4.2 21 | - explicit support for Python 3.5 (Felix Yan) 22 | 23 | 0.4.1 24 | - support pypy3 and python 3.4 25 | - document late binding not supported 26 | 27 | 0.4 28 | - significant optimizations 29 | - support arity introspection for unary ops 30 | Backward incompatible changes: 31 | - previous clumsy support for overloadded ops dropped 32 | - WhateverCode is now true in boolean context 33 | 34 | 0.3 35 | - added Python 3 support (ashleyh) 36 | 37 | 0.2.2 38 | ... 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014, Alexander Schepanovski. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of whatever nor the names of its contributors may 15 | be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include CHANGELOG 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | The Whatever Object 2 | =================== 3 | 4 | An easy way to make lambdas by partial application of python operators. 5 | 6 | Inspired by Perl 6 one, see http://perlcabal.org/syn/S02.html#The_Whatever_Object 7 | 8 | 9 | Usage 10 | ----- 11 | 12 | .. code:: python 13 | 14 | from whatever import _, that 15 | 16 | # get a list of guys names 17 | names = map(_.name, guys) 18 | names = map(that.name, guys) 19 | 20 | odd = map(_ * 2 + 1, range(10)) 21 | 22 | squares = map(_ ** 2, range(100)) 23 | small_squares = filter(_ < 100, squares) 24 | 25 | best = max(tries, key=_.score) 26 | sort(guys, key=-that.height) 27 | 28 | factorial = lambda n: reduce(_ * _, range(2, n+1)) 29 | 30 | NOTE: chained comparisons cannot be implemented since there is no boolean overloading in python. 31 | 32 | 33 | CAVEATS 34 | ------- 35 | 36 | In some special cases whatever can cause confusion: 37 | 38 | .. code:: python 39 | 40 | _.attr # this makes callable 41 | obj._ # this fetches '_' attribute of obj 42 | 43 | _[key] # this works too 44 | d[_] # KeyError, most probably 45 | 46 | _._ # short for attrgetter('_') 47 | _[_] # short for lambda d, k: d[k] 48 | 49 | if _ == 'Any value': 50 | # You will get here, definitely 51 | # `_ == something` produces callable, which is true 52 | 53 | [1, 2, _ * 2, None].index('hi') # => 2, since bool(_ * 2 == 'hi') is True 54 | 55 | 56 | Also, whatever sometimes fails on late binding: 57 | 58 | .. code:: python 59 | 60 | (_ * 2)('2') # -> NotImplemented 61 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='whatever', 5 | version='0.7', 6 | author='Alexander Schepanovski', 7 | author_email='suor.web@gmail.com', 8 | 9 | description='Easy way to make anonymous functions by partial application of operators.', 10 | long_description=open('README.rst').read(), 11 | url='http://github.com/Suor/whatever', 12 | license='BSD', 13 | 14 | py_modules=['whatever'], 15 | 16 | classifiers=[ 17 | 'Development Status :: 4 - Beta', 18 | 'License :: OSI Approved :: BSD License', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 2', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3.4', 25 | 'Programming Language :: Python :: 3.5', 26 | 'Programming Language :: Python :: 3.6', 27 | 'Programming Language :: Python :: 3.7', 28 | 'Programming Language :: Python :: 3.8', 29 | 'Programming Language :: Python :: 3.9', 30 | 'Programming Language :: Python :: 3.10', 31 | 'Programming Language :: Python :: 3.11', 32 | 'Programming Language :: Python :: Implementation :: CPython', 33 | 'Programming Language :: Python :: Implementation :: PyPy', 34 | 35 | 'Intended Audience :: Developers', 36 | 'Topic :: Software Development :: Libraries :: Python Modules', 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /test_whatever.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from whatever import _, that 3 | 4 | 5 | def test_basic(): 6 | assert _ is that 7 | 8 | 9 | def test_caller(): 10 | assert _(11)(lambda x: x ** 2) == 121 11 | assert _('100', base=11)(int) == 121 12 | 13 | 14 | def test_attrs(): 15 | assert callable(_.attr) 16 | 17 | class A(object): 18 | def __init__(self, name): 19 | self.name = name 20 | 21 | assert _.name(A('zebra')) == 'zebra' 22 | 23 | class B(object): 24 | def __getattr__(self, name): 25 | return name * 2 26 | 27 | b = B() 28 | assert _.foo(b) == 'foofoo' 29 | 30 | 31 | def test_getitem(): 32 | assert callable(_[0]) 33 | assert _[0]([1, 2, 3]) == 1 34 | assert _[1:]([1, 2, 3]) == [2, 3] 35 | 36 | assert callable(_['item']) 37 | assert _['name']({'name': 'zebra'}) == 'zebra' 38 | 39 | 40 | def test_add(): 41 | # last and right 42 | assert (_ + 1)(10) == 11 43 | assert (1 + _)(10) == 11 44 | 45 | # non commutative 46 | assert (_ + 'y')('x') == 'xy' 47 | assert ('y' + _)('x') == 'yx' 48 | 49 | 50 | def test_late_binding(): 51 | assert (_ * 2)(2) == 4 52 | assert (_ * 2)('2') is NotImplemented 53 | 54 | 55 | @pytest.mark.xfail 56 | def test_overloaded(): 57 | class StickyString(str): 58 | def __add__(self, other): 59 | if other is _: 60 | return NotImplemented 61 | return str(self) + str(other) 62 | 63 | def __radd__(self, other): 64 | if other is _: 65 | return NotImplemented 66 | return str(other) + str(self) 67 | 68 | foo = StickyString('foo') 69 | assert foo + 2 == 'foo2' 70 | assert (_ + 2)(foo) == 'foo2' 71 | assert (foo + _)(2) == 'foo2' 72 | assert (_ + foo)(2) == '2foo' 73 | 74 | 75 | def test_comparison(): 76 | assert callable(_ < 10) 77 | assert (_ < 10)(5) is (5 < 10) 78 | assert (_ < 10)(15) is (15 < 10) 79 | 80 | assert (_ == None)(None) is (None == None) 81 | assert (_ != None)(None) is (None != None) 82 | 83 | assert list(filter(5 < _, range(10))) == [6, 7, 8, 9] 84 | 85 | 86 | def test_unary(): 87 | assert callable(-_) 88 | assert callable(abs(_)) 89 | 90 | assert (-_)(5) == -5 91 | assert abs(_)(-5) == 5 92 | assert min([2,3,5,6], key=-_) == 6 93 | 94 | 95 | def test_chained_ops(): 96 | assert callable(_ + 1 + 2) 97 | assert (_ + 1 + 2)(10) == 13 98 | 99 | assert (_ * 2 + 3)(1) == 5 100 | assert (_ + 2 * 3)(1) == 7 101 | 102 | assert (_ + 1 + 1 + 1 + 1 + 1)(0) == 5 103 | assert (_ + 1 + 1)(1) == 3 104 | assert (1 + _ + 1)(1) == 3 105 | assert (1 + (1 + _))(1) == 3 106 | 107 | assert ( _ + 'y' + 'z')('x') == 'xyz' 108 | assert ('x' + _ + 'z')('y') == 'xyz' 109 | assert ('x' + 'y' + _ )('z') == 'xyz' 110 | 111 | assert -abs(_)(-5) == -5 112 | assert abs(3 - _)(10) == 7 113 | 114 | 115 | # NOTE: this is impossible since there is no boolean overloading in python 116 | # def test_chained_comparison(): 117 | # assert (1 < _ < 10)(7) 118 | # assert not (1 < _ < 10)(10) 119 | # assert not (1 < _ < 10)(1) 120 | 121 | 122 | def test_chained_attrs(): 123 | class A(object): 124 | def __init__(self, val): 125 | self.val = val 126 | 127 | assert callable(-that.val) 128 | assert (-that.val)(A(10)) == -10 129 | assert (that.val + 100)(A(10)) == 110 130 | 131 | assert ((_ + ', ' + 'Guys!').lower)('Hi')() == 'hi, guys!' 132 | assert that.val.val(A(A('a value'))) == 'a value' 133 | 134 | 135 | def test_chained_getitem(): 136 | assert callable(-that[0]) 137 | assert callable(-that[2:]) 138 | assert callable(that['key'] + 1) 139 | assert callable((_ + 1)['key']) 140 | 141 | assert (-_[0])([1,2,3]) == -1 142 | assert (_[1:] + [4])([1,2,3]) == [2,3,4] 143 | assert (_ + [4])[2:]([1,2,3]) == [3,4] 144 | 145 | assert (_['val'] * 5)({'val': 2}) == 10 146 | assert _['i']['j']({'i': {'j': 7}}) == 7 147 | 148 | 149 | def test_higher_cardinality(): 150 | assert (_ + _)(1, 2) == 3 151 | assert (_ + _ + 1)(1, 2) == 4 152 | assert (1 + (_ + _))(1, 2) == 4 153 | assert (_ ** _ ** _)(2, 3, 4) == 2 ** 3 ** 4 154 | assert ((_ ** _) ** _)(2, 3, 4) == (2 ** 3) ** 4 155 | assert (_ ** (_ ** _))(2, 3, 4) == 2 ** (3 ** 4) 156 | 157 | 158 | def test_introspect_whatever(): 159 | code = _.__code__ 160 | assert code.co_argcount == 1 161 | assert len(code.co_varnames) == 1 162 | assert code.co_nlocals == 1 163 | 164 | 165 | def test_introspection(): 166 | assert (_ + 1).__code__.co_argcount == 1 167 | assert (_ + _).__code__.co_argcount == 2 168 | assert (_ + 1 + _).__code__.co_argcount == 2 169 | 170 | code = (_ + _).__code__ 171 | assert 'add' in code.co_name 172 | assert len(code.co_varnames) == 2 173 | assert code.co_nlocals == 2 174 | 175 | 176 | def test_code_to_code(): 177 | assert (_ ** 2 + _ ** 2)(3, 4) == 5 ** 2 178 | 179 | 180 | def test_contains(): 181 | with pytest.raises(NotImplementedError): 1 in _ 182 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 2.7 3 | envlist = py27, py33, py34, py35, py36, py37, py38, py39, py310, py311, pypy, pypy3, flakes 4 | 5 | 6 | [testenv] 7 | deps = 8 | pytest 9 | commands = py.test 10 | 11 | 12 | [flake8] 13 | max-line-length = 100 14 | ignore = E401,E126,E127,E265,E302,E272,E261,E266,E731,E131,W503 15 | 16 | 17 | [testenv:flakes] 18 | deps = 19 | flake8 20 | commands = 21 | flake8 whatever.py 22 | flake8 --ignore=E231,E711,E20,E22,E701 test_whatever.py 23 | 24 | -------------------------------------------------------------------------------- /whatever.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The Whatever object inspired by Perl 6 one. 4 | 5 | See http://perlcabal.org/syn/S02.html#The_Whatever_Object 6 | """ 7 | import operator, sys 8 | from types import CodeType 9 | 10 | __all__ = ['_', 'that'] 11 | PY2 = sys.version_info[0] == 2 12 | PY38 = sys.version_info[:2] >= (3, 8) 13 | PY311 = sys.version_info[:2] >= (3, 11) 14 | 15 | 16 | # TODO: or not to do 17 | # object.__call__(self[, args...]) 18 | 19 | # TODO: pow with module arg: 20 | # object.__pow__(self, other[, modulo]) 21 | 22 | 23 | def _make_code(arity, name, varnames): 24 | # Python 3 got kwonlyargcount, Python 3.8 got posonlyargcount 25 | args = [arity, arity] if PY2 else [arity, 0, 0, arity] if PY38 else [arity, 0, arity] 26 | args.extend([1, 67, b'', (), (), varnames, __name__, name]) 27 | # Python 3.11 got qualname 28 | if PY311: 29 | args.append('%s.%s' % (__name__, name)) 30 | args.extend([1, b'']) 31 | # Python 3.11 got exceptiontable 32 | if PY311: 33 | args.append(b'') 34 | return CodeType(*args) 35 | 36 | 37 | class Whatever(object): 38 | def __contains__(self, other): 39 | raise NotImplementedError('Sorry, can\'t to hook "in" operator in this way') 40 | 41 | @staticmethod 42 | def __call__(*args, **kwargs): 43 | return WhateverCode.make_call(lambda f: f(*args, **kwargs), 1) 44 | 45 | __code__ = _make_code(1, 'Whatever', ('f',)) 46 | 47 | 48 | class WhateverCode(object): 49 | def __init__(self, arity): 50 | self._arity = arity 51 | 52 | @classmethod 53 | def make_call(cls, func, arity): 54 | sub_cls = type('WhateverCodeCall', (WhateverCode,), {'__call__': staticmethod(func)}) 55 | return sub_cls(arity) 56 | 57 | def __contains__(self, other): 58 | raise NotImplementedError('Sorry, can\'t hook "in" operator in this way') 59 | 60 | # Simulate normal callable 61 | @property 62 | def __code__(self): 63 | fname = self.__call__.__name__ or 'operator' 64 | varnames = tuple('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'[:self._arity]) 65 | return _make_code(self._arity, fname, varnames) 66 | 67 | 68 | ### Unary ops 69 | 70 | def unary(op): 71 | return lambda self: WhateverCode.make_call(op, 1) 72 | 73 | def code_unary(op): 74 | return lambda self: WhateverCode.make_call(lambda that: op(self(that)), self._arity) 75 | 76 | 77 | ### Binary ops 78 | 79 | def operand_type(value): 80 | return Whatever if isinstance(value, Whatever) else \ 81 | WhateverCode if isinstance(value, WhateverCode) else \ 82 | None 83 | 84 | def argcount(operand): 85 | op_type = operand_type(operand) 86 | if op_type is None: 87 | return 0 88 | elif op_type is Whatever: 89 | return 1 90 | else: 91 | return operand._arity 92 | 93 | def gen_binary(op, left, right): 94 | W, C, D = Whatever, WhateverCode, None 95 | name, rname, func, args = op 96 | ltype, rtype = types = operand_type(left), operand_type(right) 97 | 98 | # Constant incorporating optimizations 99 | lfunc = rfunc = None 100 | if ltype is D: 101 | _lfunc = lambda x: func(left, x) 102 | lfunc = getattr(left, name, _lfunc) if name else _lfunc 103 | if rtype is D: 104 | if name == '__getattr__': 105 | assert isinstance(right, str) 106 | # NOTE: eval('lambda x: x.%s' % right) is even faster, but to slow to construct 107 | rfunc = operator.attrgetter(right) 108 | elif name == '__getitem__': 109 | rfunc = operator.itemgetter(right) 110 | else: 111 | _rfunc = lambda x: func(x, right) 112 | rfunc = getattr(right, rname, _rfunc) if rname else _rfunc 113 | 114 | # Resolve embedded __call__ in advance 115 | if ltype is C: 116 | lcall = left.__call__ 117 | if rtype is C: 118 | rcall = right.__call__ 119 | largs, rargs = argcount(left), argcount(right) 120 | 121 | ops = { 122 | (W, D): rfunc, 123 | (D, W): lfunc, 124 | # (C, D) are optimized for one argument variant 125 | (C, D): (lambda x: rfunc(lcall(x))) if largs == 1 else 126 | (lambda *xs: rfunc(lcall(*xs))), 127 | (D, C): (lambda x: lfunc(rcall(x))) if rargs == 1 else 128 | (lambda *xs: lfunc(rcall(*xs))), 129 | (W, W): func, 130 | (W, C): lambda x, *ys: func(x, rcall(*ys)), 131 | (C, W): lambda *xs: func(lcall(*xs[:-1]), xs[-1]), 132 | (C, C): lambda *xs: func(lcall(*xs[:largs]), rcall(*xs[largs:])), 133 | } 134 | return WhateverCode.make_call(ops[types], arity=largs + rargs) 135 | 136 | def binary(op): 137 | return lambda left, right: gen_binary(op, left, right) 138 | 139 | def rbinary(op): 140 | return lambda left, right: gen_binary(op, right, left) 141 | 142 | 143 | ### Define ops 144 | 145 | def op(name, rname=None, func=None, args=2): 146 | name = '__%s__' % name 147 | if rname: 148 | rname = '__%s__' % rname 149 | return (name, rname, func or getattr(operator, name), args) 150 | 151 | def rop(name, func=None): 152 | return op(name, 'r' + name, func=func, args=2) 153 | 154 | def ops(names, args=2): 155 | return [op(name, args=args) for name in names] 156 | 157 | def rops(names): 158 | return [rop(name) for name in names] 159 | 160 | 161 | OPS = rops(['add', 'sub', 'mul', 'floordiv', 'truediv', 'mod', 'pow', 162 | 'lshift', 'rshift', 'and', 'xor', 'or']) \ 163 | + [rop('divmod', func=divmod)] \ 164 | + ops(['eq', 'ne', 'lt', 'le', 'gt', 'ge']) \ 165 | + ops(['neg', 'pos', 'abs', 'invert'], args=1) \ 166 | + [op('getattr', func=getattr), op('getitem')] 167 | 168 | # This things were dropped in python 3 169 | if PY2: 170 | OPS += [rop('div'), op('cmp', func=cmp)] # noqa 171 | 172 | for op in OPS: 173 | name, rname, func, args = op 174 | if args == 1: 175 | setattr(Whatever, name, unary(func)) 176 | setattr(WhateverCode, name, code_unary(func)) 177 | elif args == 2: 178 | setattr(Whatever, name, binary(op)) 179 | setattr(WhateverCode, name, binary(op)) 180 | if rname: 181 | setattr(Whatever, rname, rbinary(op)) 182 | setattr(WhateverCode, rname, rbinary(op)) 183 | 184 | _ = that = Whatever() 185 | --------------------------------------------------------------------------------