diff --git a/Doc/library/operator.rst b/Doc/library/operator.rst --- a/Doc/library/operator.rst +++ b/Doc/library/operator.rst @@ -433,6 +433,33 @@ | Ordering | ``a > b`` | ``gt(a, b)`` | +-----------------------+-------------------------+---------------------------------------+ +.. attribute:: symbol_map + + Implements the above table as a :class:`dict` mapping symbols to functions. + For example, ``operator.symbol_map['+='] is operator.iadd``. In the case of + the unary operators :func:`neg`, :func:`pos`, and :func:`invert`, the symbol + follows the convention of the table, requiring ``'-a'``, ``'+a'``, and + ``'~a'``, respectively. The :func:`getitem`, :func:`setitem`, and + :func:`delitem` entries do the same, using ``obj``, ``k``, and ``v`` as + necessary. :func:`truth` is entered as ``'if a'``. + +.. function:: get_op(symbol) + + Return a suitable function to implement the given symbol. + + If symbol is a key in symbol_map, the mapped function is returned. + + If symbol starts with '.', an attrgetter instance is created and returned + instead. + + If symbol is a valid Python literal enclosed by square brackets, an + itemgetter instance is created and returned. + + Note that it is only possible to get a single-argument attrgetter or + itemgetter instance. + + + Inplace Operators ----------------- diff --git a/Lib/operator.py b/Lib/operator.py --- a/Lib/operator.py +++ b/Lib/operator.py @@ -18,7 +18,7 @@ 'is_not', 'isub', 'itemgetter', 'itruediv', 'ixor', 'le', 'length_hint', 'lshift', 'lt', 'methodcaller', 'mod', 'mul', 'ne', 'neg', 'not_', 'or_', 'pos', 'pow', 'rshift', 'setitem', 'sub', - 'truediv', 'truth', 'xor'] + 'truediv', 'truth', 'xor', 'symbol_map', 'get_op'] from builtins import abs as _abs @@ -410,3 +410,96 @@ __isub__ = isub __itruediv__ = itruediv __ixor__ = ixor + +# Symbol-to-op Mapping ********************************************************# + +symbol_map = { + # comparison ops + '<': lt, + '<=': le, + '==': eq, + '!=': ne, + '>=': ge, + '>': gt, + + # logical ops + 'not': not_, + 'if a': truth, + 'is': is_, + 'is not': is_not, + + # math/bitwise ops + '+': add, + '&': and_, + '//': floordiv, + 'index': index, + '~a': inv, + '~ a': inv, + '<<': lshift, + '%': mod, + '*': mul, + '-a': neg, + '- a': neg, + '|': or_, + '+a': pos, + '+ a': pos, + '**': pow, + '>>': rshift, + '-': sub, + '/': truediv, + '^': xor, + + # seq ops + 'in': contains, + 'obj[k] = v': setitem, + 'obj[k]=v': setitem, + 'del obj[k]': delitem, + 'obj[k]': getitem, + + # in-place ops + '+=': iadd, + '&=': iand, + '//=': ifloordiv, + '<<=': ilshift, + '%=': imod, + '*=': imul, + '|=': ior, + '**=': ipow, + '>>=': irshift, + '-=': isub, + '/=': itruediv, + '^=': ixor, + } + +def get_op(symbol): + """Return a suitable function to implement the given symbol. + + If symbol is a key in symbol_map, the mapped function is returned. + + If symbol starts with '.', an attrgetter instance is created and returned + instead. + + If symbol is a valid Python literal enclosed by square brackets, an + itemgetter instance is created and returned. + + Note that it is only possible to get a single-argument attrgetter or + itemgetter instance. + """ + + func = symbol_map.get(symbol) + + if func is not None: + return func + + if symbol.startswith('.'): + return attrgetter(symbol[1:]) + + if symbol.startswith('[') and symbol.endswith(']'): + from ast import literal_eval + try: + return itemgetter(literal_eval(symbol[1:-1])) + except SyntaxError: + msg = '{!r} is not an acceptable item to get'.format(symbol[1:-1]) + raise ValueError(msg) from None + + raise ValueError('{!r} is not a valid symbol'.format(symbol)) diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -476,6 +476,26 @@ class PyOperatorTestCase(OperatorTestCase, unittest.TestCase): module = py_operator + def test_get_op(self): + operator = self.module + self.assertIs(operator.get_op('+'), operator.add) + self.assertIs(operator.get_op('+='), operator.iadd) + + self.assertRaises(ValueError, operator.get_op, '=') + + self.assertIsInstance(operator.get_op('.attr'), operator.attrgetter) + self.assertIsInstance(operator.get_op('[3]'), operator.itemgetter) + + self.assertRaises(ValueError, operator.get_op, '[1:2]') + + ag = operator.get_op('.add') + self.assertIs(ag(operator), operator.add) + + ig = operator.get_op('[2]') + word = 'testing' + self.assertEqual(ig(word), word[2]) + + @unittest.skipUnless(c_operator, 'requires _operator') class COperatorTestCase(OperatorTestCase, unittest.TestCase): module = c_operator