diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 90214a314031d1..0b43c9a386fca5 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -881,6 +881,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(decoder)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(default)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(defaultaction)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(defaults)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(delete)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(depth)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(desired_access)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 97a75d0c46c867..d0449881daf5e3 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -370,6 +370,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(decoder) STRUCT_FOR_ID(default) STRUCT_FOR_ID(defaultaction) + STRUCT_FOR_ID(defaults) STRUCT_FOR_ID(delete) STRUCT_FOR_ID(depth) STRUCT_FOR_ID(desired_access) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 4f928cc050bf8e..b66226cd4fb483 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -879,6 +879,7 @@ extern "C" { INIT_ID(decoder), \ INIT_ID(default), \ INIT_ID(defaultaction), \ + INIT_ID(defaults), \ INIT_ID(delete), \ INIT_ID(depth), \ INIT_ID(desired_access), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index 5b78d038fc1192..3b793056d71adf 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1276,6 +1276,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(defaults); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(delete); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/operator.py b/Lib/operator.py index 1b765522f85949..99b5503430e4c5 100644 --- a/Lib/operator.py +++ b/Lib/operator.py @@ -14,10 +14,10 @@ 'delitem', 'eq', 'floordiv', 'ge', 'getitem', 'gt', 'iadd', 'iand', 'iconcat', 'ifloordiv', 'ilshift', 'imatmul', 'imod', 'imul', 'index', 'indexOf', 'inv', 'invert', 'ior', 'ipow', 'irshift', - 'is_', 'is_none', 'is_not', 'is_not_none', 'isub', 'itemgetter', 'itruediv', - 'ixor', 'le', 'length_hint', 'lshift', 'lt', 'matmul', 'methodcaller', 'mod', - 'mul', 'ne', 'neg', 'not_', 'or_', 'pos', 'pow', 'rshift', - 'setitem', 'sub', 'truediv', 'truth', 'xor'] + 'is_', 'is_none', 'is_not', 'is_not_none', 'isub', 'itemgetter', + 'itemtuplegetter', 'itruediv', 'ixor', 'le', 'length_hint', 'lshift', 'lt', + 'matmul', 'methodcaller', 'mod', 'mul', 'ne', 'neg', 'not_', 'or_', 'pos', + 'pow', 'rshift', 'setitem', 'sub', 'truediv', 'truth', 'xor'] from builtins import abs as _abs @@ -307,6 +307,81 @@ def __repr__(self): def __reduce__(self): return self.__class__, self._items +class itemtuplegetter: + """ + Return a callable object that fetches the given items from its operand in a tuple. + + If defaults is given, when called on an object where i-th `items` is not present, + the corresponding defaults is returned instead. If the defaults iterable is + shorter than subscripts iterable, the remaining subscripts have no defaults. + If the defaults iterable is longer than subscripts iterable, extra defaults are + ignored. + + The returned callable has two read-only properties: + operator.itemtuplegetter.items: a tuple containing items to fetch + operator.itemtuplegetter.defaults: a tuple containing provided defaults + + For example, + After f = itemtuplegetter([0, 2], defaults=(-1, -2)), f([1, 2]) evaluates to (1, -2). + After g = itemtuplegetter([0, 2], defaults=(-1)), f([1, 2]) resutls in an IndexError. + After h = itemtuplegetter([0], defaults=(-1, -2)), f([1, 2]) evaluates to (1,). + After i = itemtuplegetter([1, 0], defaults=(-1, -2)), f([1, 2]) evaluates to (2, 1). + """ + __slots__ = ('_items', '_defaults') + + def __init__(self, items, /, defaults=None): + self._items = tuple(items) if not isinstance(items, tuple) else items + if defaults is None: + self._defaults = () + elif isinstance(defaults, tuple): + self._defaults = defaults[:len(self._items)] + elif isinstance(defaults, (list, range)): + self._defaults = tuple(defaults[:len(self._items)]) + else: + def islice(iterable): + for _, default in zip(self._items, defaults, strict=False): + yield default + + self._defaults = tuple(islice(defaults)) + + @property + def items(self): + return self._items + + @property + def defaults(self): + return self._defaults + + def __call__(self, obj, /): + if not (defaults := self._defaults): + return tuple(obj[item] for item in self._items) + else: + result = [] + append = result.append + items = iter(self._items) + for default in defaults: + try: + item = next(items) + except StopIteration: + return tuple(result) + try: + append(obj[item]) + except (IndexError, KeyError): + append(default) + + for item in items: + append(obj[item]) + return tuple(result) + + def __repr__(self): + return '%s.%s(%s, defaults=%s)' % (self.__class__.__module__, + self.__class__.__name__, + repr(self._items), + repr(self._defaults)) + + def __reduce__(self): + return self.__class__, (self._items, self._defaults) + class methodcaller: """ Return a callable object that calls the given method on its operand. diff --git a/Lib/test/test_operator.py b/Lib/test/test_operator.py index 82578a0ef1e6f2..7036e4a5712679 100644 --- a/Lib/test/test_operator.py +++ b/Lib/test/test_operator.py @@ -471,6 +471,171 @@ class T(tuple): self.assertEqual(operator.itemgetter(0)(['a', 'b', 'c']), 'a') self.assertEqual(operator.itemgetter(0)(range(100, 200)), 100) + def test_itemtuplegetter(self): + operator = self.module + self.assertRaises(TypeError, operator.itemtuplegetter, 2) + self.assertRaises(TypeError, operator.itemtuplegetter, 2, 2) + self.assertRaises(TypeError, operator.itemtuplegetter, 2, 2,) + self.assertRaises(TypeError, operator.itemtuplegetter, (2,), 2) + a = 'ABCDE' + f = operator.itemtuplegetter((2,)) + self.assertEqual(f(a), ('C',)) + self.assertRaises(TypeError, f) + self.assertRaises(TypeError, f, a, 3) + self.assertRaises(TypeError, f, a, size=3) + f = operator.itemtuplegetter((10,)) + self.assertRaises(IndexError, f, a) + f = operator.itemtuplegetter((10,), defaults=(0,)) + self.assertEqual(f(a), (0,)) + + + class C(object): + def __getitem__(self, name): + raise SyntaxError + self.assertRaises(SyntaxError, operator.itemtuplegetter((42,)), C()) + + f = operator.itemtuplegetter(('name',)) + self.assertRaises(TypeError, f, a) + self.assertRaises(TypeError, operator.itemtuplegetter) + + d = dict(key='val') + f = operator.itemtuplegetter(('key',)) + self.assertEqual(f(d), ('val',)) + f = operator.itemtuplegetter(('nonkey',)) + self.assertRaises(KeyError, f, d) + + # example used in the docs + inventory = [('apple', 3), ('banana', 2), ('pear', 5), ('orange', 1)] + sorter = operator.itemtuplegetter((1, 0)) + self.assertEqual(list(map(sorter, inventory)), + [(3, 'apple'), (2, 'banana'), (5, 'pear'), (1, 'orange')]) + self.assertEqual(sorted(inventory, key=sorter), + [('orange', 1), ('banana', 2), ('apple', 3), ('pear', 5)]) + + # multiple gets + data = list(map(str, range(20))) + self.assertEqual(operator.itemtuplegetter((2,10,5))(data), ('2', '10', '5')) + self.assertRaises(TypeError, operator.itemtuplegetter((2, 'x', 5)), data) + + # interesting indices + t = tuple('abcde') + self.assertEqual(operator.itemtuplegetter((-1,))(t), ('e',)) + self.assertEqual(operator.itemtuplegetter((slice(2, 4),))(t), (('c', 'd'),)) + + # defaults + inventory = dict(inventory) + seq = range(10) + + kwd_default = operator.itemtuplegetter(("banana", "mango"), defaults=(0, -1)) + pos_default = operator.itemtuplegetter(("banana", "mango"), (0, -1)) + # positional default + self.assertEqual(kwd_default(inventory), (2, -1)) + self.assertEqual(pos_default(inventory), (2, -1)) + + # defaults is None by default, and None is treated as empty tuple + f1 = operator.itemtuplegetter(()) + f2 = operator.itemtuplegetter((), None) + f3 = operator.itemtuplegetter((), ()) + + self.assertEqual(repr(f1), repr(f2)) + self.assertEqual(f1.defaults, f2.defaults) + + self.assertEqual(repr(f1), repr(f3)) + self.assertEqual(f1.defaults, f3.defaults) + + # default shorter than items and items missing + self.assertRaises(KeyError, + operator.itemtuplegetter(("banana", "mango"), defaults=(0,)), + inventory) + self.assertRaises(IndexError, + operator.itemtuplegetter((9, 10), defaults=(0,)), + seq) + # default shorter than items, but missing item has default + self.assertEqual(operator.itemtuplegetter(("mango", "banana"), defaults=(0,))(inventory), + (0, 2)) + # default longer than items and items missing + self.assertEqual(operator.itemtuplegetter(("banana", "mango"), defaults=(0, -1, 0))(inventory), + (2, -1)) + self.assertEqual(operator.itemtuplegetter((9, 10), defaults=(0, -1, 0))(seq), + (9, -1)) + # default shorter than items and items present + self.assertEqual(operator.itemtuplegetter(("banana", "pear"), defaults=(0, -1, 0))(inventory), + (2, 5)) + self.assertEqual(operator.itemtuplegetter((8, 9), defaults=(0, -1, 0))(seq), + (8, 9)) + + # iterable variants + seq = range(10) + base = operator.itemtuplegetter((10, 12, 5), defaults=(-1, -2)) + + ## iterators + self.assertEqual(operator.itemtuplegetter(iter([10, 12, 5]), defaults=iter((-1, -2)))(seq), + base(seq)) + ## lists + self.assertEqual(operator.itemtuplegetter([10, 12, 5], defaults=[-1, -2])(seq), + base(seq)) + ## tuple subclass + class T(tuple): + 'Tuple subclass' + pass + self.assertEqual(operator.itemtuplegetter(T((10, 12, 5)), defaults=T((-1, -2)))(seq), + base(seq)) + ## custom iterable + self.assertEqual(operator.itemtuplegetter(Seq1([10, 12, 5]), defaults=Seq2([-1, -2]))(seq), + base(seq)) + ## generator + range + g = (i for i in [10, 12, 5]) + self.assertEqual(operator.itemtuplegetter(g, defaults=range(-1, -3, -1))(seq), + base(seq)) + ## validate base + self.assertEqual(base(seq), (-1, -2, 5)) + + ## inifinite iterators as defaults + def natural_numbers(): + start = 1 + while True: + yield start + start += 1 + self.assertEqual(operator.itemtuplegetter([0, 5, 4, 2], defaults=natural_numbers())("abcd"), + ("a", 2, 3, "c")) + + ## str + self.assertEqual(operator.itemtuplegetter([10, 12, 5], defaults="abc")(seq), + ("a", "b", 5)) + + # interesting init + EMPTY_TUPLE = () + self.assertEqual(operator.itemtuplegetter(EMPTY_TUPLE)(seq), EMPTY_TUPLE) + self.assertEqual(operator.itemtuplegetter(EMPTY_TUPLE, EMPTY_TUPLE)(seq), EMPTY_TUPLE) + self.assertEqual(operator.itemtuplegetter(EMPTY_TUPLE, EMPTY_TUPLE)(EMPTY_TUPLE), EMPTY_TUPLE) + self.assertEqual(operator.itemtuplegetter(iter(EMPTY_TUPLE), EMPTY_TUPLE)(EMPTY_TUPLE), EMPTY_TUPLE) + self.assertEqual(operator.itemtuplegetter(iter(EMPTY_TUPLE), None)(EMPTY_TUPLE), EMPTY_TUPLE) + + # attributes + itg = operator.itemtuplegetter((1, 2, 3)) + self.assertEqual(itg.items, (1, 2, 3)) + self.assertEqual(itg.defaults, ()) + itg = operator.itemtuplegetter((1, 2, 3), defaults=None) + self.assertEqual(itg.items, (1, 2, 3)) + self.assertEqual(itg.defaults, ()) + itg = operator.itemtuplegetter(iter([1]), defaults=range(2)) + self.assertEqual(itg.items, (1,)) + self.assertEqual(itg.defaults, (0,)) + ## extra defaults are ignored + itg = operator.itemtuplegetter([], defaults=(1, 2, 3, 4, 5)) + self.assertEqual(itg.items, ()) + self.assertEqual(itg.defaults, ()) + + self.assertRaises(AttributeError, setattr, itg, "items", 2) + self.assertRaises(AttributeError, setattr, itg, "defaults", 2) + + with self.assertRaises(AttributeError): + itg.items = 2 + with self.assertRaises(AttributeError): + itg.items += (2,) + with self.assertRaises(AttributeError): + itg.defaults += (2,) + def test_methodcaller(self): operator = self.module self.assertRaises(TypeError, operator.methodcaller) @@ -650,6 +815,13 @@ def test_itemgetter_signature(self): sig = inspect.signature(operator.itemgetter(2, 3, 5)) self.assertEqual(str(sig), '(obj, /)') + def test_itemtuplegetter_signature(self): + operator = self.module + sig = inspect.signature(operator.itemtuplegetter) + self.assertEqual(str(sig), '(items, /, defaults=None)') + sig = inspect.signature(operator.itemtuplegetter((2, 3, 5))) + self.assertEqual(str(sig), '(obj, /)') + def test_methodcaller_signature(self): operator = self.module sig = inspect.signature(operator.methodcaller) @@ -716,6 +888,25 @@ def test_itemgetter(self): self.assertEqual(repr(f2), repr(f)) self.assertEqual(f2(a), f(a)) + def test_itemtuplegetter(self): + itemtuplegetter = self.module.itemtuplegetter + a = 'ABCDE' + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(proto=proto): + f = itemtuplegetter((1, 3)) + f2 = self.copy(f, proto) + self.assertEqual(repr(f2), repr(f)) + self.assertEqual(f2(a), f(a)) + self.assertEqual(f2.defaults, f.defaults) + self.assertEqual(f2.items, f.items) + # multiple gets + f = itemtuplegetter((1, 3), defaults=iter(range(3))) + f2 = self.copy(f, proto) + self.assertEqual(repr(f2), repr(f)) + self.assertEqual(f2(a), f(a)) + self.assertEqual(f2.defaults, f.defaults) + self.assertEqual(f2.items, f.items) + def test_methodcaller(self): methodcaller = self.module.methodcaller class A: diff --git a/Modules/_operator.c b/Modules/_operator.c index ce3ef015710223..9cd919dd1bac42 100644 --- a/Modules/_operator.c +++ b/Modules/_operator.c @@ -9,6 +9,7 @@ typedef struct { PyObject *itemgetter_type; + PyObject *itemtuplegetter_type; PyObject *attrgetter_type; PyObject *methodcaller_type; } _operator_state; @@ -23,8 +24,10 @@ get_operator_state(PyObject *module) /*[clinic input] module _operator +class _operator.itemtuplegetter "itemtuplegetterobject *" "&itemtuplegetter_type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=672ecf48487521e7]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=b774962bc68a4012]*/ + PyDoc_STRVAR(operator_doc, "Operator interface.\n\ @@ -1230,6 +1233,294 @@ static PyType_Spec itemgetter_type_spec = { .slots = itemgetter_type_slots, }; +/* itemtuplegetter object **********************************************************/ + +typedef struct { + PyObject_HEAD + PyObject *items; + PyObject *defaults; + Py_ssize_t nitems; + Py_ssize_t ndefaults; +} itemtuplegetterobject; + +// Forward declarations +static PyObject * +itemtuplegetter_call_impl(itemtuplegetterobject *, PyObject *); + +/*[clinic input] +@classmethod +_operator.itemtuplegetter.__new__ as itemtuplegetter_new + + items as itbl: object + iterable of items to get from an object + / + defaults as defaultitbl: object = None + iterable of defaults to replace of missing items, if None treated as () + +Return a callable object that fetches the given items from its operand in a tuple. + +If defaults is given, when called on an object where i-th `items` is not present, +the corresponding defaults is returned instead. If the defaults iterable is +shorter than subscripts iterable, the remaining subscripts have no defaults. +If the defaults iterable is longer than subscripts iterable, extra defaults are +ignored. + +The returned callable has two read-only properties: + operator.itemtuplegetter.items: a tuple containing items to fetch + operator.itemtuplegetter.defaults: a tuple containing provided defaults + +For example, +After f = itemtuplegetter([0, 2], defaults=(-1, -2)), f([1, 2]) evaluates to (1, -2). +After g = itemtuplegetter([0, 2], defaults=(-1)), f([1, 2]) resutls in an IndexError. +After h = itemtuplegetter([0], defaults=(-1, -2)), f([1, 2]) evaluates to (1,). +After i = itemtuplegetter([1, 0], defaults=(-1, -2)), f([1, 2]) evaluates to (2, 1). +[clinic start generated code]*/ + +static PyObject * +itemtuplegetter_new_impl(PyTypeObject *type, PyObject *itbl, + PyObject *defaultitbl) +/*[clinic end generated code: output=37d6a97f69ed1fc1 input=45599a5989a9fa76]*/ + +{ + itemtuplegetterobject *itg; + PyObject *items = NULL; + PyObject *defaults = NULL; + Py_ssize_t nitems; + Py_ssize_t ndefaults; + PyObject *it; + + items = PySequence_Tuple(itbl); + if (items == NULL) { + return NULL; + } + + nitems = PyTuple_GET_SIZE(items); + + if (defaultitbl == Py_None) { + defaults = PyTuple_New(0); + if (defaults == NULL) + return NULL; + } + else if (PySequence_Check(defaultitbl)) { + PyObject *tup = PySequence_Tuple(defaultitbl); + if (tup == NULL) { + Py_XDECREF(defaults); + return NULL; + } + PyObject *slice = PySequence_GetSlice(tup, 0u, nitems); + if (slice == NULL) { + Py_XDECREF(defaults); + Py_XDECREF(tup); + return NULL; + } + defaults = slice; + } + else { + Py_ssize_t j; + it = PyObject_GetIter(defaultitbl); + if (it == NULL) + return NULL; + + /* Guess result size and allocate space. */ + Py_ssize_t n = PyObject_LengthHint(defaultitbl, nitems); + if (n == -1) + goto Fail; + n = (n < nitems) ? n : nitems; + defaults = PyTuple_New(n); + if (defaults == NULL) + goto Fail; + + /* Fill the tuple. */ + for (j = 0; j < n; ++j) { + PyObject *item = PyIter_Next(it); + if (item == NULL) { + if (PyErr_Occurred()) + goto Fail; + break; + } + PyTuple_SET_ITEM(defaults, j, item); + } + + /* Cut tuple back if guess was too large. */ + if (j < n && _PyTuple_Resize(&defaults, j) != 0) + goto Fail; + Py_DECREF(it); + } + ndefaults = PyTuple_GET_SIZE(defaults); + + _operator_state *state = _PyType_GetModuleState(type); + /* create itemtuplegetterobject structure */ + itg = PyObject_GC_New(itemtuplegetterobject, (PyTypeObject *) state->itemtuplegetter_type); + if (itg == NULL) { + return NULL; + } + + itg->items = Py_NewRef(items); + itg->nitems = nitems; + itg->defaults = defaults; + itg->ndefaults = ndefaults; + + PyObject_GC_Track(itg); + return (PyObject *)itg; + + Fail: + Py_XDECREF(defaults); + Py_DECREF(it); + return NULL; +} + +static int +itemtuplegetter_clear(itemtuplegetterobject *itg) +{ + Py_CLEAR(itg->items); + Py_CLEAR(itg->defaults); + return 0; +} + +static void +itemtuplegetter_dealloc(itemtuplegetterobject *itg) +{ + PyTypeObject *tp = Py_TYPE(itg); + PyObject_GC_UnTrack(itg); + (void)itemtuplegetter_clear(itg); + tp->tp_free(itg); + Py_DECREF(tp); +} + +static int +itemtuplegetter_traverse(itemtuplegetterobject *itg, visitproc visit, void *arg) +{ + Py_VISIT(Py_TYPE(itg)); + Py_VISIT(itg->items); + Py_VISIT(itg->defaults); + return 0; +} + +static PyObject * +itemtuplegetter_call(itemtuplegetterobject *itg, PyObject *args, PyObject *kw) +{ + assert(PyTuple_CheckExact(args)); + if (!_PyArg_NoKeywords("itemtuplegetter", kw)) + return NULL; + if (!_PyArg_CheckPositional("itemtuplegetter", PyTuple_GET_SIZE(args), 1, 1)) + return NULL; + return itemtuplegetter_call_impl(itg, PyTuple_GET_ITEM(args, 0)); +} + +static PyObject * +itemtuplegetter_call_impl(itemtuplegetterobject *itg, PyObject *obj) +{ + PyObject *result; + Py_ssize_t nitems=itg->nitems; + Py_ssize_t ndefaults=itg->ndefaults; + + result = PyTuple_New(nitems); + if (result == NULL) + return NULL; + + if (nitems == 0) + return result; + + Py_ssize_t i = 0; + + if (ndefaults > 0) { + PyObject *item, *val, *found; + for (i=0 ; i < ndefaults; i++) { + item = PyTuple_GET_ITEM(itg->items, i); + found = PyObject_GetItem(obj, item); + if (found == NULL) { + val = PyTuple_GET_ITEM(itg->defaults, i); + PyErr_Clear(); + Py_INCREF(val); + } + else { + val = found; + } + PyTuple_SET_ITEM(result, i, val); + } + + if (ndefaults == nitems) + return result; + } + + for (; i < nitems; i++) { + PyObject *item, *val; + + item = PyTuple_GET_ITEM(itg->items, i); + val = PyObject_GetItem(obj, item); + if (val == NULL) { + Py_DECREF(result); + return NULL; + } + PyTuple_SET_ITEM(result, i, val); + } + return result; +} + +static PyObject * +itemtuplegetter_repr(itemtuplegetterobject *itg) +{ + PyObject *repr; + const char *reprfmt; + + int status = Py_ReprEnter((PyObject *)itg); + if (status != 0) { + if (status < 0) + return NULL; + return PyUnicode_FromFormat("%s(...)", Py_TYPE(itg)->tp_name); + } + + reprfmt = "%s(%R, defaults=%R)"; + repr = PyUnicode_FromFormat(reprfmt, Py_TYPE(itg)->tp_name, itg->items, itg->defaults); + Py_ReprLeave((PyObject *)itg); + return repr; +} + +static PyObject * +itemtuplegetter_reduce(itemtuplegetterobject *itg, PyObject *Py_UNUSED(ignored)) +{ + return Py_BuildValue("O(OO)", Py_TYPE(itg), itg->items, itg->defaults); +} + +static PyMethodDef itemtuplegetter_methods[] = { + {"__reduce__", (PyCFunction)itemtuplegetter_reduce, METH_NOARGS, + reduce_doc}, + {NULL} +}; + +static PyMemberDef itemtuplegetter_members[] = { + {"items", _Py_T_OBJECT, offsetof(itemtuplegetterobject, items), Py_READONLY, + "tuple of items to get from object in future itemtuplegetter calls"}, + {"defaults", _Py_T_OBJECT, offsetof(itemtuplegetterobject, defaults), Py_READONLY, + "tuple of defaults as in case of absent items in future itemtuplegetter calls"}, + {NULL} /* Sentinel */ +}; + +static PyType_Slot itemtuplegetter_type_slots[] = { + {Py_tp_dealloc, itemtuplegetter_dealloc}, + {Py_tp_call, itemtuplegetter_call}, + {Py_tp_doc, (void *)itemtuplegetter_new__doc__}, + {Py_tp_traverse, itemtuplegetter_traverse}, + {Py_tp_clear, itemtuplegetter_clear}, + {Py_tp_methods, itemtuplegetter_methods}, + {Py_tp_members, itemtuplegetter_members}, + {Py_tp_getset, common_getset}, + {Py_tp_new, itemtuplegetter_new}, + {Py_tp_getattro, PyObject_GenericGetAttr}, + {Py_tp_repr, itemtuplegetter_repr}, + {0, 0} +}; + +static PyType_Spec itemtuplegetter_type_spec = { + .name = "operator.itemtuplegetter", + .basicsize = sizeof(itemtuplegetterobject), + .itemsize = 0, + .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_IMMUTABLETYPE), + .slots = itemtuplegetter_type_slots, +}; + + /* attrgetter object **********************************************************/ typedef struct { @@ -1943,6 +2234,15 @@ operator_exec(PyObject *module) return -1; } + state->itemtuplegetter_type = PyType_FromModuleAndSpec(module, &itemtuplegetter_type_spec, NULL); + if (state->itemtuplegetter_type == NULL) { + return -1; + } + if (PyModule_AddType(module, (PyTypeObject *)state->itemtuplegetter_type) < 0) { + return -1; + } + + state->methodcaller_type = PyType_FromModuleAndSpec(module, &methodcaller_type_spec, NULL); if (state->methodcaller_type == NULL) { return -1; @@ -1968,6 +2268,7 @@ operator_traverse(PyObject *module, visitproc visit, void *arg) _operator_state *state = get_operator_state(module); Py_VISIT(state->attrgetter_type); Py_VISIT(state->itemgetter_type); + Py_VISIT(state->itemtuplegetter_type); Py_VISIT(state->methodcaller_type); return 0; } @@ -1978,6 +2279,7 @@ operator_clear(PyObject *module) _operator_state *state = get_operator_state(module); Py_CLEAR(state->attrgetter_type); Py_CLEAR(state->itemgetter_type); + Py_CLEAR(state->itemtuplegetter_type); Py_CLEAR(state->methodcaller_type); return 0; } diff --git a/Modules/clinic/_operator.c.h b/Modules/clinic/_operator.c.h index 48a8ea8c3379ab..e1bf82a1471513 100644 --- a/Modules/clinic/_operator.c.h +++ b/Modules/clinic/_operator.c.h @@ -2,6 +2,10 @@ preserve [clinic start generated code]*/ +#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) +# include "pycore_gc.h" // PyGC_Head +# include "pycore_runtime.h" // _Py_ID() +#endif #include "pycore_abstract.h" // _PyNumber_Index() #include "pycore_modsupport.h" // _PyArg_CheckPositional() @@ -1507,4 +1511,88 @@ _operator__compare_digest(PyObject *module, PyObject *const *args, Py_ssize_t na exit: return return_value; } -/*[clinic end generated code: output=972e2543c4fcf1ba input=a9049054013a1b77]*/ + +PyDoc_STRVAR(itemtuplegetter_new__doc__, +"itemtuplegetter(items, /, defaults=None)\n" +"--\n" +"\n" +"Return a callable object that fetches the given items from its operand in a tuple.\n" +"\n" +" items\n" +" iterable of items to get from an object\n" +" defaults\n" +" iterable of defaults to replace of missing items, if None treated as ()\n" +"\n" +"If defaults is given, when called on an object where i-th `items` is not present,\n" +"the corresponding defaults is returned instead. If the defaults iterable is\n" +"shorter than subscripts iterable, the remaining subscripts have no defaults.\n" +"If the defaults iterable is longer than subscripts iterable, extra defaults are\n" +"ignored.\n" +"\n" +"The returned callable has two read-only properties:\n" +" operator.itemtuplegetter.items: a tuple containing items to fetch\n" +" operator.itemtuplegetter.defaults: a tuple containing provided defaults\n" +"\n" +"For example,\n" +"After f = itemtuplegetter([0, 2], defaults=(-1, -2)), f([1, 2]) evaluates to (1, -2).\n" +"After g = itemtuplegetter([0, 2], defaults=(-1)), f([1, 2]) resutls in an IndexError.\n" +"After h = itemtuplegetter([0], defaults=(-1, -2)), f([1, 2]) evaluates to (1,).\n" +"After i = itemtuplegetter([1, 0], defaults=(-1, -2)), f([1, 2]) evaluates to (2, 1)."); + +static PyObject * +itemtuplegetter_new_impl(PyTypeObject *type, PyObject *itbl, + PyObject *defaultitbl); + +static PyObject * +itemtuplegetter_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(defaults), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "defaults", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "itemtuplegetter", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[2]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 1; + PyObject *itbl; + PyObject *defaultitbl = Py_None; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + itbl = fastargs[0]; + if (!noptargs) { + goto skip_optional_pos; + } + defaultitbl = fastargs[1]; +skip_optional_pos: + return_value = itemtuplegetter_new_impl(type, itbl, defaultitbl); + +exit: + return return_value; +} +/*[clinic end generated code: output=fecd71fb7c77a77d input=a9049054013a1b77]*/