From 6b6aa774a8c6ea5e4c1ec223f8c1d3b5947761cb Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Thu, 25 May 2023 21:38:18 -0500 Subject: [PATCH 1/6] Add deprecations for itertool pickle support. --- Modules/itertoolsmodule.c | 43 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 555eab09935e9e..26b652d122e265 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -93,6 +93,15 @@ class itertools.pairwise "pairwiseobject *" "clinic_state()->pairwise_type" #undef clinic_state_by_cls #undef clinic_state +/* Deprecation of pickle support: GH-101588 *********************************/ + +#define ITERTOOL_PICKLE_DEPRECATION \ + if (PyErr_WarnEx( \ + PyExc_DeprecationWarning, \ + "Itertool pickle support will be removed in a Python 3.14.", 1) < 0) { \ + Py_RETURN_NONE; \ + } + /* batched object ************************************************************/ /* Note: The built-in zip() function includes a "strict" argument @@ -506,6 +515,7 @@ groupby_reduce(groupbyobject *lz, PyObject *Py_UNUSED(ignored)) /* reduce as a 'new' call with an optional 'setstate' if groupby * has started */ + ITERTOOL_PICKLE_DEPRECATION; PyObject *value; if (lz->tgtkey && lz->currkey && lz->currvalue) value = Py_BuildValue("O(OO)(OOO)", Py_TYPE(lz), @@ -522,6 +532,7 @@ PyDoc_STRVAR(reduce_doc, "Return state information for pickling."); static PyObject * groupby_setstate(groupbyobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; PyObject *currkey, *currvalue, *tgtkey; if (!PyTuple_Check(state)) { PyErr_SetString(PyExc_TypeError, "state is not a tuple"); @@ -660,6 +671,7 @@ _grouper_next(_grouperobject *igo) static PyObject * _grouper_reduce(_grouperobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; if (((groupbyobject *)lz->parent)->currgrouper != lz) { return Py_BuildValue("N(())", _PyEval_GetBuiltin(&_Py_ID(iter))); } @@ -828,6 +840,7 @@ teedataobject_dealloc(teedataobject *tdo) static PyObject * teedataobject_reduce(teedataobject *tdo, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; int i; /* create a temporary list of already iterated values */ PyObject *values = PyList_New(tdo->numread); @@ -1041,12 +1054,14 @@ tee_dealloc(teeobject *to) static PyObject * tee_reduce(teeobject *to, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; return Py_BuildValue("O(())(Oi)", Py_TYPE(to), to->dataobj, to->index); } static PyObject * tee_setstate(teeobject *to, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; teedataobject *tdo; int index; if (!PyTuple_Check(state)) { @@ -1275,6 +1290,7 @@ cycle_next(cycleobject *lz) static PyObject * cycle_reduce(cycleobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; /* Create a new cycle with the iterator tuple, then set the saved state */ if (lz->it == NULL) { PyObject *it = PyObject_GetIter(lz->saved); @@ -1298,6 +1314,7 @@ cycle_reduce(cycleobject *lz, PyObject *Py_UNUSED(ignored)) static PyObject * cycle_setstate(cycleobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; PyObject *saved=NULL; int firstpass; if (!PyTuple_Check(state)) { @@ -1446,12 +1463,14 @@ dropwhile_next(dropwhileobject *lz) static PyObject * dropwhile_reduce(dropwhileobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; return Py_BuildValue("O(OO)l", Py_TYPE(lz), lz->func, lz->it, lz->start); } static PyObject * dropwhile_setstate(dropwhileobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; int start = PyObject_IsTrue(state); if (start < 0) return NULL; @@ -1584,12 +1603,14 @@ takewhile_next(takewhileobject *lz) static PyObject * takewhile_reduce(takewhileobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; return Py_BuildValue("O(OO)l", Py_TYPE(lz), lz->func, lz->it, lz->stop); } static PyObject * takewhile_reduce_setstate(takewhileobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; int stop = PyObject_IsTrue(state); if (stop < 0) @@ -1786,6 +1807,7 @@ islice_next(isliceobject *lz) static PyObject * islice_reduce(isliceobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; /* When unpickled, generate a new object with the same bounds, * then 'setstate' with the next and count */ @@ -1818,6 +1840,7 @@ islice_reduce(isliceobject *lz, PyObject *Py_UNUSED(ignored)) static PyObject * islice_setstate(isliceobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; Py_ssize_t cnt = PyLong_AsSsize_t(state); if (cnt == -1 && PyErr_Occurred()) @@ -1953,6 +1976,7 @@ starmap_next(starmapobject *lz) static PyObject * starmap_reduce(starmapobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; /* Just pickle the iterator */ return Py_BuildValue("O(OO)", Py_TYPE(lz), lz->func, lz->it); } @@ -2109,6 +2133,7 @@ chain_next(chainobject *lz) static PyObject * chain_reduce(chainobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; if (lz->source) { /* we can't pickle function objects (itertools.from_iterable) so * we must use setstate to replace the iterable. One day we @@ -2128,6 +2153,7 @@ chain_reduce(chainobject *lz, PyObject *Py_UNUSED(ignored)) static PyObject * chain_setstate(chainobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; PyObject *source, *active=NULL; if (!PyTuple_Check(state)) { @@ -2403,6 +2429,7 @@ product_next(productobject *lz) static PyObject * product_reduce(productobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; if (lz->stopped) { return Py_BuildValue("O(())", Py_TYPE(lz)); } else if (lz->result == NULL) { @@ -2433,6 +2460,7 @@ product_reduce(productobject *lz, PyObject *Py_UNUSED(ignored)) static PyObject * product_setstate(productobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; PyObject *result; Py_ssize_t n, i; @@ -2711,6 +2739,7 @@ combinations_next(combinationsobject *co) static PyObject * combinations_reduce(combinationsobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; if (lz->result == NULL) { return Py_BuildValue("O(On)", Py_TYPE(lz), lz->pool, lz->r); } else if (lz->stopped) { @@ -2740,6 +2769,7 @@ combinations_reduce(combinationsobject *lz, PyObject *Py_UNUSED(ignored)) static PyObject * combinations_setstate(combinationsobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; PyObject *result; Py_ssize_t i; Py_ssize_t n = PyTuple_GET_SIZE(lz->pool); @@ -3019,6 +3049,7 @@ cwr_next(cwrobject *co) static PyObject * cwr_reduce(cwrobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; if (lz->result == NULL) { return Py_BuildValue("O(On)", Py_TYPE(lz), lz->pool, lz->r); } else if (lz->stopped) { @@ -3047,6 +3078,7 @@ cwr_reduce(cwrobject *lz, PyObject *Py_UNUSED(ignored)) static PyObject * cwr_setstate(cwrobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; PyObject *result; Py_ssize_t n, i; @@ -3354,6 +3386,7 @@ permutations_next(permutationsobject *po) static PyObject * permutations_reduce(permutationsobject *po, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; if (po->result == NULL) { return Py_BuildValue("O(On)", Py_TYPE(po), po->pool, po->r); } else if (po->stopped) { @@ -3396,6 +3429,7 @@ permutations_reduce(permutationsobject *po, PyObject *Py_UNUSED(ignored)) static PyObject * permutations_setstate(permutationsobject *po, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; PyObject *indices, *cycles, *result; Py_ssize_t n, i; @@ -3593,6 +3627,7 @@ accumulate_next(accumulateobject *lz) static PyObject * accumulate_reduce(accumulateobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; itertools_state *state = lz->state; if (lz->initial != Py_None) { @@ -3628,6 +3663,7 @@ accumulate_reduce(accumulateobject *lz, PyObject *Py_UNUSED(ignored)) static PyObject * accumulate_setstate(accumulateobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; Py_INCREF(state); Py_XSETREF(lz->total, state); Py_RETURN_NONE; @@ -3776,6 +3812,7 @@ compress_next(compressobject *lz) static PyObject * compress_reduce(compressobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; return Py_BuildValue("O(OO)", Py_TYPE(lz), lz->data, lz->selectors); } @@ -3908,6 +3945,7 @@ filterfalse_next(filterfalseobject *lz) static PyObject * filterfalse_reduce(filterfalseobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; return Py_BuildValue("O(OO)", Py_TYPE(lz), lz->func, lz->it); } @@ -4135,6 +4173,7 @@ count_repr(countobject *lz) static PyObject * count_reduce(countobject *lz, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; if (lz->cnt == PY_SSIZE_T_MAX) return Py_BuildValue("O(OO)", Py_TYPE(lz), lz->long_cnt, lz->long_step); return Py_BuildValue("O(n)", Py_TYPE(lz), lz->cnt); @@ -4258,6 +4297,7 @@ PyDoc_STRVAR(length_hint_doc, "Private method returning an estimate of len(list( static PyObject * repeat_reduce(repeatobject *ro, PyObject *Py_UNUSED(ignored)) { + ITERTOOL_PICKLE_DEPRECATION; /* unpickle this so that a new repeat iterator is constructed with an * object, then call __setstate__ on it to set cnt */ @@ -4478,7 +4518,7 @@ zip_longest_next(ziplongestobject *lz) static PyObject * zip_longest_reduce(ziplongestobject *lz, PyObject *Py_UNUSED(ignored)) { - + ITERTOOL_PICKLE_DEPRECATION; /* Create a new tuple with empty sequences where appropriate to pickle. * Then use setstate to set the fillvalue */ @@ -4505,6 +4545,7 @@ zip_longest_reduce(ziplongestobject *lz, PyObject *Py_UNUSED(ignored)) static PyObject * zip_longest_setstate(ziplongestobject *lz, PyObject *state) { + ITERTOOL_PICKLE_DEPRECATION; Py_INCREF(state); Py_XSETREF(lz->fillvalue, state); Py_RETURN_NONE; From c24e2934b28d96fb37b545053f5296622c886c61 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 26 May 2023 01:18:05 -0500 Subject: [PATCH 2/6] Limit category to a DeprecationWarnings --- Lib/test/test_itertools.py | 43 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_itertools.py b/Lib/test/test_itertools.py index 9fe559d4b7eed5..c799e6cf59dc49 100644 --- a/Lib/test/test_itertools.py +++ b/Lib/test/test_itertools.py @@ -15,6 +15,20 @@ import struct import threading import gc +import warnings + +def pickle_deprecated(testfunc): + """ Run the test twice. On the first pass, verify that a + Deprecation Warning is raised. On the second pass, run + normally but with DeprecationWarnings temporarily disabled. + """ + def inner(self): + with self.assertWarns(DeprecationWarning): + testfunc(self) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + testfunc(self) + return inner maxsize = support.MAX_Py_ssize_t minsize = -maxsize-1 @@ -124,6 +138,7 @@ def expand(it, i=0): c = expand(compare[took:]) self.assertEqual(a, c); + @pickle_deprecated def test_accumulate(self): self.assertEqual(list(accumulate(range(10))), # one positional arg [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]) @@ -220,6 +235,7 @@ def test_chain_from_iterable(self): self.assertRaises(TypeError, list, chain.from_iterable([2, 3])) self.assertEqual(list(islice(chain.from_iterable(repeat(range(5))), 2)), [0, 1]) + @pickle_deprecated def test_chain_reducible(self): for oper in [copy.deepcopy] + picklecopiers: it = chain('abc', 'def') @@ -233,6 +249,7 @@ def test_chain_reducible(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): self.pickletest(proto, chain('abc', 'def'), compare=list('abcdef')) + @pickle_deprecated def test_chain_setstate(self): self.assertRaises(TypeError, chain().__setstate__, ()) self.assertRaises(TypeError, chain().__setstate__, []) @@ -246,6 +263,7 @@ def test_chain_setstate(self): it.__setstate__((iter(['abc', 'def']), iter(['ghi']))) self.assertEqual(list(it), ['ghi', 'a', 'b', 'c', 'd', 'e', 'f']) + @pickle_deprecated def test_combinations(self): self.assertRaises(TypeError, combinations, 'abc') # missing r argument self.assertRaises(TypeError, combinations, 'abc', 2, 1) # too many arguments @@ -269,7 +287,6 @@ def test_combinations(self): self.assertEqual(list(op(testIntermediate)), [(0,1,3), (0,2,3), (1,2,3)]) - def combinations1(iterable, r): 'Pure python version shown in the docs' pool = tuple(iterable) @@ -337,6 +354,7 @@ def test_combinations_tuple_reuse(self): self.assertEqual(len(set(map(id, combinations('abcde', 3)))), 1) self.assertNotEqual(len(set(map(id, list(combinations('abcde', 3))))), 1) + @pickle_deprecated def test_combinations_with_replacement(self): cwr = combinations_with_replacement self.assertRaises(TypeError, cwr, 'abc') # missing r argument @@ -425,6 +443,7 @@ def test_combinations_with_replacement_tuple_reuse(self): self.assertEqual(len(set(map(id, cwr('abcde', 3)))), 1) self.assertNotEqual(len(set(map(id, list(cwr('abcde', 3))))), 1) + @pickle_deprecated def test_permutations(self): self.assertRaises(TypeError, permutations) # too few arguments self.assertRaises(TypeError, permutations, 'abc', 2, 1) # too many arguments @@ -531,6 +550,7 @@ def test_combinatorics(self): self.assertEqual(comb, list(filter(set(perm).__contains__, cwr))) # comb: cwr that is a perm self.assertEqual(comb, sorted(set(cwr) & set(perm))) # comb: both a cwr and a perm + @pickle_deprecated def test_compress(self): self.assertEqual(list(compress(data='ABCDEF', selectors=[1,0,1,0,1,1])), list('ACEF')) self.assertEqual(list(compress('ABCDEF', [1,0,1,0,1,1])), list('ACEF')) @@ -564,7 +584,7 @@ def test_compress(self): next(testIntermediate) self.assertEqual(list(op(testIntermediate)), list(result2)) - + @pickle_deprecated def test_count(self): self.assertEqual(lzip('abc',count()), [('a', 0), ('b', 1), ('c', 2)]) self.assertEqual(lzip('abc',count(3)), [('a', 3), ('b', 4), ('c', 5)]) @@ -613,6 +633,7 @@ def test_count(self): #check proper internal error handling for large "step' sizes count(1, maxsize+5); sys.exc_info() + @pickle_deprecated def test_count_with_stride(self): self.assertEqual(lzip('abc',count(2,3)), [('a', 2), ('b', 5), ('c', 8)]) self.assertEqual(lzip('abc',count(start=2,step=3)), @@ -675,6 +696,7 @@ def test_cycle(self): self.assertRaises(TypeError, cycle, 5) self.assertEqual(list(islice(cycle(gen3()),10)), [0,1,2,0,1,2,0,1,2,0]) + @pickle_deprecated def test_cycle_copy_pickle(self): # check copy, deepcopy, pickle c = cycle('abc') @@ -711,6 +733,7 @@ def test_cycle_copy_pickle(self): d = pickle.loads(p) # rebuild the cycle object self.assertEqual(take(20, d), list('cdeabcdeabcdeabcdeab')) + @pickle_deprecated def test_cycle_unpickle_compat(self): testcases = [ b'citertools\ncycle\n(c__builtin__\niter\n((lI1\naI2\naI3\natRI1\nbtR((lI1\naI0\ntb.', @@ -742,6 +765,7 @@ def test_cycle_unpickle_compat(self): it = pickle.loads(t) self.assertEqual(take(10, it), [2, 3, 1, 2, 3, 1, 2, 3, 1, 2]) + @pickle_deprecated def test_cycle_setstate(self): # Verify both modes for restoring state @@ -778,6 +802,7 @@ def test_cycle_setstate(self): self.assertRaises(TypeError, cycle('').__setstate__, ()) self.assertRaises(TypeError, cycle('').__setstate__, ([],)) + @pickle_deprecated def test_groupby(self): # Check whether it accepts arguments correctly self.assertEqual([], list(groupby([]))) @@ -935,6 +960,7 @@ def test_filter(self): c = filter(isEven, range(6)) self.pickletest(proto, c) + @pickle_deprecated def test_filterfalse(self): self.assertEqual(list(filterfalse(isEven, range(6))), [1,3,5]) self.assertEqual(list(filterfalse(None, [0,1,0,2,0])), [0,0,0]) @@ -965,6 +991,7 @@ def test_zip(self): lzip('abc', 'def')) @support.impl_detail("tuple reuse is specific to CPython") + @pickle_deprecated def test_zip_tuple_reuse(self): ids = list(map(id, zip('abc', 'def'))) self.assertEqual(min(ids), max(ids)) @@ -1040,6 +1067,7 @@ def test_zip_longest_tuple_reuse(self): ids = list(map(id, list(zip_longest('abc', 'def')))) self.assertEqual(len(dict.fromkeys(ids)), len(ids)) + @pickle_deprecated def test_zip_longest_pickling(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): self.pickletest(proto, zip_longest("abc", "def")) @@ -1186,6 +1214,7 @@ def test_product_tuple_reuse(self): self.assertEqual(len(set(map(id, product('abc', 'def')))), 1) self.assertNotEqual(len(set(map(id, list(product('abc', 'def'))))), 1) + @pickle_deprecated def test_product_pickling(self): # check copy, deepcopy, pickle for args, result in [ @@ -1201,6 +1230,7 @@ def test_product_pickling(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): self.pickletest(proto, product(*args)) + @pickle_deprecated def test_product_issue_25021(self): # test that indices are properly clamped to the length of the tuples p = product((1, 2),(3,)) @@ -1211,6 +1241,7 @@ def test_product_issue_25021(self): p.__setstate__((0, 0, 0x1000)) # will access tuple element 1 if not clamped self.assertRaises(StopIteration, next, p) + @pickle_deprecated def test_repeat(self): self.assertEqual(list(repeat(object='a', times=3)), ['a', 'a', 'a']) self.assertEqual(lzip(range(3),repeat('a')), @@ -1243,6 +1274,7 @@ def test_repeat_with_negative_times(self): self.assertEqual(repr(repeat('a', times=-1)), "repeat('a', 0)") self.assertEqual(repr(repeat('a', times=-2)), "repeat('a', 0)") + @pickle_deprecated def test_map(self): self.assertEqual(list(map(operator.pow, range(3), range(1,7))), [0**1, 1**2, 2**3]) @@ -1273,6 +1305,7 @@ def test_map(self): c = map(tupleize, 'abc', count()) self.pickletest(proto, c) + @pickle_deprecated def test_starmap(self): self.assertEqual(list(starmap(operator.pow, zip(range(3), range(1,7)))), [0**1, 1**2, 2**3]) @@ -1300,6 +1333,7 @@ def test_starmap(self): c = starmap(operator.pow, zip(range(3), range(1,7))) self.pickletest(proto, c) + @pickle_deprecated def test_islice(self): for args in [ # islice(args) should agree with range(args) (10, 20, 3), @@ -1394,6 +1428,7 @@ def __index__(self): self.assertEqual(list(islice(range(100), IntLike(10), IntLike(50), IntLike(5))), list(range(10,50,5))) + @pickle_deprecated def test_takewhile(self): data = [1, 3, 5, 20, 2, 4, 6, 8] self.assertEqual(list(takewhile(underten, data)), [1, 3, 5]) @@ -1414,6 +1449,7 @@ def test_takewhile(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): self.pickletest(proto, takewhile(underten, data)) + @pickle_deprecated def test_dropwhile(self): data = [1, 3, 5, 20, 2, 4, 6, 8] self.assertEqual(list(dropwhile(underten, data)), [20, 2, 4, 6, 8]) @@ -1431,6 +1467,7 @@ def test_dropwhile(self): for proto in range(pickle.HIGHEST_PROTOCOL + 1): self.pickletest(proto, dropwhile(underten, data)) + @pickle_deprecated def test_tee(self): n = 200 @@ -1732,6 +1769,7 @@ class TestExamples(unittest.TestCase): def test_accumulate(self): self.assertEqual(list(accumulate([1,2,3,4,5])), [1, 3, 6, 10, 15]) + @pickle_deprecated def test_accumulate_reducible(self): # check copy, deepcopy, pickle data = [1, 2, 3, 4, 5] @@ -1747,6 +1785,7 @@ def test_accumulate_reducible(self): self.assertEqual(list(copy.deepcopy(it)), accumulated[1:]) self.assertEqual(list(copy.copy(it)), accumulated[1:]) + @pickle_deprecated def test_accumulate_reducible_none(self): # Issue #25718: total is None it = accumulate([None, None, None], operator.is_) From 168700523b3ce6c8808c273b1fa7aaa7fcb65e05 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 26 May 2023 01:20:37 -0500 Subject: [PATCH 3/6] Expand warning message to cover copy and deepcopy as well --- Modules/itertoolsmodule.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 26b652d122e265..81cbe3241a50c0 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -95,11 +95,12 @@ class itertools.pairwise "pairwiseobject *" "clinic_state()->pairwise_type" /* Deprecation of pickle support: GH-101588 *********************************/ -#define ITERTOOL_PICKLE_DEPRECATION \ - if (PyErr_WarnEx( \ - PyExc_DeprecationWarning, \ - "Itertool pickle support will be removed in a Python 3.14.", 1) < 0) { \ - Py_RETURN_NONE; \ +#define ITERTOOL_PICKLE_DEPRECATION \ + if (PyErr_WarnEx( \ + PyExc_DeprecationWarning, \ + "Itertool pickle/copy/deepcopy support " \ + "will be removed in a Python 3.14.", 1) < 0) { \ + Py_RETURN_NONE; \ } /* batched object ************************************************************/ From ae7f17bc05e67dac170a0977ed3909c64b75e0ec Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 26 May 2023 01:31:51 -0500 Subject: [PATCH 4/6] Add whatsnew entry --- Doc/whatsnew/3.12.rst | 6 ++++++ .../Library/2023-05-26-01-31-30.gh-issue-101588.RaqxFy.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-05-26-01-31-30.gh-issue-101588.RaqxFy.rst diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 5266f5ffb93737..ef8327d06c245b 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -998,6 +998,12 @@ Pending Removal in Python 3.14 functions that have been deprecated since Python 2 but only gained a proper :exc:`DeprecationWarning` in 3.12. Remove them in 3.14. +* :mod:`itertools` had undocumented, inefficient, historically buggy, + and inconsistent support for copy, deepcopy, and pickle operations. + This will be removed in 3.14 with a significant reduction in code + volume and maintenance burden. + (Contributed by Raymond Hettinger in :gh:`101588`.) + * Accessing ``co_lnotab`` was deprecated in :pep:`626` since 3.10 and was planned to be removed in 3.12 but it only got a proper :exc:`DeprecationWarning` in 3.12. diff --git a/Misc/NEWS.d/next/Library/2023-05-26-01-31-30.gh-issue-101588.RaqxFy.rst b/Misc/NEWS.d/next/Library/2023-05-26-01-31-30.gh-issue-101588.RaqxFy.rst new file mode 100644 index 00000000000000..07e3dc468f7d9a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-05-26-01-31-30.gh-issue-101588.RaqxFy.rst @@ -0,0 +1 @@ +Deprecate undocumented copy/deepcopy/pickle support for itertools. From 4fefb7ee622fc90ffb7c0c12202f44e17d114759 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 26 May 2023 11:11:42 -0500 Subject: [PATCH 5/6] Return NULL instead of None. Add third pass to check warning-to-error promotion. --- Lib/test/test_itertools.py | 12 +++++++++--- Modules/itertoolsmodule.c | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_itertools.py b/Lib/test/test_itertools.py index c799e6cf59dc49..4d6ea780e15373 100644 --- a/Lib/test/test_itertools.py +++ b/Lib/test/test_itertools.py @@ -18,9 +18,10 @@ import warnings def pickle_deprecated(testfunc): - """ Run the test twice. On the first pass, verify that a - Deprecation Warning is raised. On the second pass, run - normally but with DeprecationWarnings temporarily disabled. + """ Run the test three times. + First, verify that a Deprecation Warning is raised. + Second, run normally but with DeprecationWarnings temporarily disabled. + Third, run with warnings promoted to errors. """ def inner(self): with self.assertWarns(DeprecationWarning): @@ -28,6 +29,11 @@ def inner(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", category=DeprecationWarning) testfunc(self) + with warnings.catch_warnings(): + warnings.simplefilter("error", category=DeprecationWarning) + with self.assertRaises((DeprecationWarning, AssertionError, SystemError)): + testfunc(self) + return inner maxsize = support.MAX_Py_ssize_t diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index 81cbe3241a50c0..4a6d1314b3864e 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -100,7 +100,7 @@ class itertools.pairwise "pairwiseobject *" "clinic_state()->pairwise_type" PyExc_DeprecationWarning, \ "Itertool pickle/copy/deepcopy support " \ "will be removed in a Python 3.14.", 1) < 0) { \ - Py_RETURN_NONE; \ + return NULL; \ } /* batched object ************************************************************/ From 8943817a61a722524534bb98d43be22d219ab2f8 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Fri, 26 May 2023 11:13:05 -0500 Subject: [PATCH 6/6] Minor wording tweak --- Doc/whatsnew/3.12.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index ef8327d06c245b..30176cb3fc15e2 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -1000,7 +1000,7 @@ Pending Removal in Python 3.14 * :mod:`itertools` had undocumented, inefficient, historically buggy, and inconsistent support for copy, deepcopy, and pickle operations. - This will be removed in 3.14 with a significant reduction in code + This will be removed in 3.14 for a significant reduction in code volume and maintenance burden. (Contributed by Raymond Hettinger in :gh:`101588`.)