From 220f1fd34732dd040493ab248f380b19852b17a8 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Sun, 16 Mar 2025 07:57:31 +0000 Subject: [PATCH 1/5] Make graphlib.TopologicalSorter.prepare() idempotent Fixes #130914 --- Lib/graphlib.py | 4 +++- Lib/test/test_graphlib.py | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Lib/graphlib.py b/Lib/graphlib.py index 1438a5fc54b9cb..378bcac6d6b8dc 100644 --- a/Lib/graphlib.py +++ b/Lib/graphlib.py @@ -92,7 +92,9 @@ def prepare(self): therefore no more nodes can be added using "add". """ if self._ready_nodes is not None: - raise ValueError("cannot prepare() more than once") + if self._npassedout > 0: + raise ValueError("cannot prepare() after starting sort") + return self._ready_nodes = [ i.node for i in self._node2info.values() if i.npredecessors == 0 diff --git a/Lib/test/test_graphlib.py b/Lib/test/test_graphlib.py index 5f38af4024c5b0..1f8615c4980372 100644 --- a/Lib/test/test_graphlib.py +++ b/Lib/test/test_graphlib.py @@ -140,7 +140,13 @@ def test_calls_before_prepare(self): def test_prepare_multiple_times(self): ts = graphlib.TopologicalSorter() ts.prepare() - with self.assertRaisesRegex(ValueError, r"cannot prepare\(\) more than once"): + ts.prepare() + + def test_prepare_after_pass_out(self): + ts = graphlib.TopologicalSorter({'a': 'bc'}) + ts.prepare() + self.assertEqual(set(ts.get_ready()), {'b', 'c'}) + with self.assertRaisesRegex(ValueError, r"cannot prepare\(\) after starting sort"): ts.prepare() def test_invalid_nodes_in_done(self): From 60e0004838f7f9588e16d906e907e66e91f60fdd Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Sun, 16 Mar 2025 08:04:35 +0000 Subject: [PATCH 2/5] Add blurb and what's new --- Doc/whatsnew/3.14.rst | 9 +++++++++ .../2025-03-16-08-00-29.gh-issue-130914.6z883_.rst | 3 +++ 2 files changed, 12 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-03-16-08-00-29.gh-issue-130914.6z883_.rst diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 9ac0e6ed2a6d40..cfbcbbd7182e15 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -465,6 +465,15 @@ getopt * Add support for returning intermixed options and non-option arguments in order. (Contributed by Serhiy Storchaka in :gh:`126390`.) + +graphlib +-------- + +* Allow :meth:`graphlib.TopologicalSorter.prepare` to be called more than once + as long as sorting has not started. + (Contributed by Daniel Pope in :gh:`130914`) + + http ---- diff --git a/Misc/NEWS.d/next/Library/2025-03-16-08-00-29.gh-issue-130914.6z883_.rst b/Misc/NEWS.d/next/Library/2025-03-16-08-00-29.gh-issue-130914.6z883_.rst new file mode 100644 index 00000000000000..956cf83758f9a2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-03-16-08-00-29.gh-issue-130914.6z883_.rst @@ -0,0 +1,3 @@ +Allow :meth:`graphlib.TopologicalSorter.prepare` to be called more than once +as long as sorting has not started. +Patch by Daniel Pope. From f32e19665aa3a85395bbeaa4efd137aec944d7ff Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Mon, 17 Mar 2025 09:49:54 +0000 Subject: [PATCH 3/5] Raise CycleError each time prepare() is called --- Lib/graphlib.py | 16 +++++++++------- Lib/test/test_graphlib.py | 6 ++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Lib/graphlib.py b/Lib/graphlib.py index 378bcac6d6b8dc..a0769d91b9f4b0 100644 --- a/Lib/graphlib.py +++ b/Lib/graphlib.py @@ -90,15 +90,17 @@ def prepare(self): still be used to obtain as many nodes as possible until cycles block more progress. After a call to this function, the graph cannot be modified and therefore no more nodes can be added using "add". + + Raise ValueError if nodes have already been passed out of the sorter. + """ - if self._ready_nodes is not None: - if self._npassedout > 0: - raise ValueError("cannot prepare() after starting sort") - return + if self._npassedout > 0: + raise ValueError("cannot prepare() after starting sort") - self._ready_nodes = [ - i.node for i in self._node2info.values() if i.npredecessors == 0 - ] + if self._ready_nodes is None: + self._ready_nodes = [ + i.node for i in self._node2info.values() if i.npredecessors == 0 + ] # ready_nodes is set before we look for cycles on purpose: # if the user wants to catch the CycleError, that's fine, # they can continue using the instance to grab as many diff --git a/Lib/test/test_graphlib.py b/Lib/test/test_graphlib.py index 1f8615c4980372..66722e0b0498a6 100644 --- a/Lib/test/test_graphlib.py +++ b/Lib/test/test_graphlib.py @@ -149,6 +149,12 @@ def test_prepare_after_pass_out(self): with self.assertRaisesRegex(ValueError, r"cannot prepare\(\) after starting sort"): ts.prepare() + def test_prepare_cycleerror_each_time(self): + ts = graphlib.TopologicalSorter({'a': 'b', 'b': 'a'}) + for attempt in range(1, 4): + with self.assertRaises(graphlib.CycleError, msg=f"{attempt=}"): + ts.prepare() + def test_invalid_nodes_in_done(self): ts = graphlib.TopologicalSorter() ts.add(1, 2, 3, 4) From 6d634547ae1e8df08598fc6fac5458ae2e66c1db Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Mon, 17 Mar 2025 09:54:57 +0000 Subject: [PATCH 4/5] Document when prepare() can be called --- Doc/library/graphlib.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Doc/library/graphlib.rst b/Doc/library/graphlib.rst index a0b16576fad219..49424700303fdd 100644 --- a/Doc/library/graphlib.rst +++ b/Doc/library/graphlib.rst @@ -106,6 +106,14 @@ function, the graph cannot be modified, and therefore no more nodes can be added using :meth:`~TopologicalSorter.add`. + A :exc:`ValueError` will be raised if the sort has been started by + :meth:`~.static_order` or :meth:`~.get_ready`. + + .. versionchanged:: next + + ``prepare()`` can now be called more than once as long as the sort has + not started. Previously this raised :exc:`ValueError`. + .. method:: is_active() Returns ``True`` if more progress can be made and ``False`` otherwise. From 32f22c71d31145a0c0cf1359c7b2aea716565b87 Mon Sep 17 00:00:00 2001 From: Daniel Pope Date: Mon, 17 Mar 2025 09:55:07 +0000 Subject: [PATCH 5/5] Add myself to ACKS --- Misc/ACKS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/ACKS b/Misc/ACKS index 27480a1f3131bd..ec7781005d26ae 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1477,6 +1477,7 @@ Michael Pomraning Martin Pool Iustin Pop Claudiu Popa +Daniel Pope Nick Pope John Popplewell Matheus Vieira Portela