From abf7168f572cb42db0783fac02763869be43201d Mon Sep 17 00:00:00 2001 From: Ali Alimohammadi <41567902+AliAlimohammadi@users.noreply.github.com> Date: Mon, 18 May 2026 14:53:15 -0700 Subject: [PATCH 01/13] feat: add Segment Intersection algorithm (#14416) * feat: add Segment Intersection algorithm * fix: use descriptive parameter names --------- Co-authored-by: John Law --- geometry/segment_intersection.py | 112 +++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 geometry/segment_intersection.py diff --git a/geometry/segment_intersection.py b/geometry/segment_intersection.py new file mode 100644 index 000000000000..e2e2e10f1e4d --- /dev/null +++ b/geometry/segment_intersection.py @@ -0,0 +1,112 @@ +""" +Given two line segments, determine whether they intersect. + +This is based on the algorithm described in Introduction to Algorithms +(CLRS), Chapter 33. + +Reference: + - https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection + - https://en.wikipedia.org/wiki/Orientation_(geometry) +""" + +from __future__ import annotations + +from typing import NamedTuple + + +class Point(NamedTuple): + """A point in 2D space. + + >>> Point(0, 0) + Point(x=0, y=0) + >>> Point(1, -3) + Point(x=1, y=-3) + """ + + x: float + y: float + + +def direction(pivot: Point, target: Point, query: Point) -> float: + """Return the cross product of vectors (pivot->query) and (pivot->target). + + The sign of the result encodes the orientation of the ordered triple + (pivot, target, query): + - Negative -> counter-clockwise (left turn) + - Positive -> clockwise (right turn) + - Zero -> collinear + + >>> direction(Point(0, 0), Point(1, 0), Point(0, 1)) + -1 + >>> direction(Point(0, 0), Point(0, 1), Point(1, 0)) + 1 + >>> direction(Point(0, 0), Point(1, 1), Point(2, 2)) + 0 + """ + return (query.x - pivot.x) * (target.y - pivot.y) - (target.x - pivot.x) * ( + query.y - pivot.y + ) + + +def on_segment(seg_start: Point, seg_end: Point, point: Point) -> bool: + """Check whether *point*, known to be collinear with the segment, lies on it. + + >>> on_segment(Point(0, 0), Point(4, 4), Point(2, 2)) + True + >>> on_segment(Point(0, 0), Point(4, 4), Point(5, 5)) + False + >>> on_segment(Point(0, 0), Point(4, 0), Point(2, 0)) + True + """ + return min(seg_start.x, seg_end.x) <= point.x <= max( + seg_start.x, seg_end.x + ) and min(seg_start.y, seg_end.y) <= point.y <= max(seg_start.y, seg_end.y) + + +def segments_intersect(p1: Point, p2: Point, p3: Point, p4: Point) -> bool: + """Return True if line segment p1p2 intersects line segment p3p4. + + Uses the CLRS cross-product / orientation method. Handles both the + general case (proper crossing) and degenerate cases where one endpoint + lies exactly on the other segment. + + >>> segments_intersect(Point(0, 0), Point(2, 2), Point(0, 2), Point(2, 0)) + True + >>> segments_intersect(Point(0, 0), Point(2, 2), Point(1, 1), Point(3, 3)) + True + >>> segments_intersect(Point(0, 0), Point(1, 0), Point(2, 0), Point(3, 0)) + False + >>> segments_intersect(Point(0, 0), Point(1, 1), Point(1, 0), Point(2, 1)) + False + >>> segments_intersect(Point(0, 0), Point(1, 1), Point(0, 1), Point(0, 2)) + False + >>> segments_intersect(Point(0, 0), Point(1, 0), Point(1, 0), Point(2, 0)) + True + """ + d1 = direction(p3, p4, p1) + d2 = direction(p3, p4, p2) + d3 = direction(p1, p2, p3) + d4 = direction(p1, p2, p4) + + if ((d1 < 0 < d2) or (d2 < 0 < d1)) and ((d3 < 0 < d4) or (d4 < 0 < d3)): + return True + + if d1 == 0 and on_segment(p3, p4, p1): + return True + if d2 == 0 and on_segment(p3, p4, p2): + return True + if d3 == 0 and on_segment(p1, p2, p3): + return True + return d4 == 0 and on_segment(p1, p2, p4) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + print("Enter four points as 'x y' pairs (one per line):") + points = [Point(*map(float, input().split())) for _ in range(4)] + p1, p2, p3, p4 = points + result = segments_intersect(p1, p2, p3, p4) + print(1 if result else 0) From 144ef9c022d556b3546de2ece425da709e50ec0c Mon Sep 17 00:00:00 2001 From: Alessandro Molinari Date: Tue, 19 May 2026 00:14:42 +0200 Subject: [PATCH 02/13] Fix type hints in sorts/tim_sort.py, relates to #14457 (#14474) * Add type hints to tim_sort.py, relates to #14457 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix ruff PYI041 error: use float instead of int | float * Fix mypy error: support str and tuple inputs as defined in doctests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix ruff E501: wrap binary_search parameters to respect 88 char limit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Refactor generics to use Python 3.12 type parameter syntax (PEP 695) * Use Any from typing to resolve mypy list unpacking bugs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: John Law --- sorts/tim_sort.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/sorts/tim_sort.py b/sorts/tim_sort.py index 41ab4a10a87b..2eeed88b7399 100644 --- a/sorts/tim_sort.py +++ b/sorts/tim_sort.py @@ -1,4 +1,7 @@ -def binary_search(lst, item, start, end): +from typing import Any + + +def binary_search(lst: list[Any], item: Any, start: int, end: int) -> int: if start == end: return start if lst[start] > item else start + 1 if start > end: @@ -13,7 +16,7 @@ def binary_search(lst, item, start, end): return mid -def insertion_sort(lst): +def insertion_sort(lst: list[Any]) -> list[Any]: length = len(lst) for index in range(1, length): @@ -24,7 +27,7 @@ def insertion_sort(lst): return lst -def merge(left, right): +def merge(left: list[Any], right: list[Any]) -> list[Any]: if not left: return right @@ -37,7 +40,7 @@ def merge(left, right): return [right[0], *merge(left, right[1:])] -def tim_sort(lst): +def tim_sort(lst: list[Any] | tuple[Any, ...] | str) -> list[Any]: """ >>> tim_sort("Python") ['P', 'h', 'n', 'o', 't', 'y'] @@ -53,7 +56,7 @@ def tim_sort(lst): length = len(lst) runs, sorted_runs = [], [] new_run = [lst[0]] - sorted_array = [] + sorted_array: list[Any] = [] i = 1 while i < length: if lst[i] < lst[i - 1]: From 33a8e0f21ae492364db3786048de0b824b7c38d3 Mon Sep 17 00:00:00 2001 From: Ali Alimohammadi <41567902+AliAlimohammadi@users.noreply.github.com> Date: Tue, 19 May 2026 16:43:43 -0700 Subject: [PATCH 03/13] feat: add Ramer-Douglas-Peucker polyline simplification algorithm (#14372) * feat: add Ramer-Douglas-Peucker polyline simplification algorithm * Use descriptive parameter names * Update geometry/ramer_douglas_peucker.py * Update geometry/ramer_douglas_peucker.py * Update ramer_douglas_peucker.py * Update ramer_douglas_peucker.py * Update ramer_douglas_peucker.py --------- Co-authored-by: John Law --- geometry/ramer_douglas_peucker.py | 184 ++++++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 geometry/ramer_douglas_peucker.py diff --git a/geometry/ramer_douglas_peucker.py b/geometry/ramer_douglas_peucker.py new file mode 100644 index 000000000000..a03bbb2e5086 --- /dev/null +++ b/geometry/ramer_douglas_peucker.py @@ -0,0 +1,184 @@ +""" +Ramer-Douglas-Peucker polyline simplification algorithm. + +Given a sequence of 2-D points and a tolerance epsilon, the algorithm +reduces the number of points while preserving the overall shape of the curve. + +Time complexity: O(n log n) average, O(n²) worst case +Space complexity: O(n) + +References: + https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm +""" + +from __future__ import annotations + +import math + + +def _euclidean_distance( + point_a: tuple[float, float], + point_b: tuple[float, float], +) -> float: + """Return the Euclidean distance between two 2-D points. + + >>> _euclidean_distance((0.0, 0.0), (3.0, 4.0)) + 5.0 + >>> _euclidean_distance((1.0, 1.0), (1.0, 1.0)) + 0.0 + """ + return math.hypot(point_b[0] - point_a[0], point_b[1] - point_a[1]) + + +def _perpendicular_distance( + point: tuple[float, float], + line_start: tuple[float, float], + line_end: tuple[float, float], +) -> float: + """Return the distance from *point* to the line **segment** between + *line_start* and *line_end*. + + When the perpendicular projection of *point* onto the infinite line falls + within the segment, this equals the perpendicular distance to that line. + When the projection falls outside the segment, the distance to the nearest + endpoint is returned instead (projection clamped to [0, 1]). + + This is the correct distance measure for the Ramer-Douglas-Peucker + algorithm: using the infinite-line distance can incorrectly discard points + whose projection lies beyond a segment endpoint. + + >>> _perpendicular_distance((4.0, 0.0), (0.0, 0.0), (0.0, 3.0)) + 4.0 + >>> # order of line_start and line_end does not affect the result + >>> _perpendicular_distance((4.0, 0.0), (0.0, 3.0), (0.0, 0.0)) + 4.0 + >>> _perpendicular_distance((4.0, 1.0), (0.0, 1.0), (0.0, 4.0)) + 4.0 + >>> _perpendicular_distance((2.0, 1.0), (-2.0, 1.0), (-2.0, 4.0)) + 4.0 + >>> # projection falls outside the segment; distance to nearest endpoint + >>> round(_perpendicular_distance((0.0, 2.0), (1.0, 0.0), (3.0, 0.0)), 6) + 2.236068 + """ + px, py = point + ax, ay = line_start + bx, by = line_end + dx, dy = bx - ax, by - ay + seg_len_sq = dx * dx + dy * dy + if seg_len_sq == 0.0: + # line_start and line_end coincide; fall back to point-to-point distance + return _euclidean_distance(point, line_start) + # Project point onto the segment line, then clamp t to [0, 1] so the + # nearest point is always on the segment rather than the infinite line. + t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / seg_len_sq)) + nearest_x = ax + t * dx + nearest_y = ay + t * dy + return math.hypot(px - nearest_x, py - nearest_y) + + +def ramer_douglas_peucker( + pts: list[tuple[float, float]], + epsilon: float, +) -> list[tuple[float, float]]: + """Simplify a polyline using the Ramer-Douglas-Peucker algorithm. + + Given a sequence of 2-D points and a maximum allowable deviation + *epsilon* (>= 0), returns a simplified list of points such that no + discarded point is farther than *epsilon* from the simplified polyline. + + Parameters + ---------- + pts: + Ordered sequence of ``(x, y)`` points describing the polyline. + epsilon: + Maximum allowable distance of any discarded point from the + simplified polyline. Must be non-negative. + + Returns + ------- + list[tuple[float, float]] + Simplified list of ``(x, y)`` points. The first and last points of + *pts* are always preserved. + + Raises + ------ + ValueError + If *epsilon* is negative. + + References + ---------- + https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm + + Examples + -------- + >>> ramer_douglas_peucker([], epsilon=1.0) + [] + >>> ramer_douglas_peucker([(0.0, 0.0)], epsilon=1.0) + [(0.0, 0.0)] + >>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 0.0)], epsilon=1.0) + [(0.0, 0.0), (1.0, 0.0)] + >>> # middle point is within epsilon - it is discarded + >>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 0.1), (2.0, 0.0)], epsilon=0.5) + [(0.0, 0.0), (2.0, 0.0)] + >>> # middle point exceeds epsilon - it is kept + >>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)], epsilon=0.5) + [(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)] + >>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 0.5), (2.0, 0.0)], epsilon=-1.0) + Traceback (most recent call last): + ... + ValueError: epsilon must be non-negative, got -1.0 + """ + if epsilon < 0: + msg = f"epsilon must be non-negative, got {epsilon!r}" + raise ValueError(msg) + + if len(pts) < 3: + return list(pts) + + # --------------------------------------------------------------------------- + # Iterative, stack-based implementation. + # + # The naive recursive approach copies sublists at every level via slicing + # (pts[:max_index+1] / pts[max_index:]), which is O(n) per call and makes + # the overall algorithm O(n²) in memory even for well-balanced splits. An + # explicit stack operating on index ranges avoids all copying and also + # eliminates the risk of hitting Python's recursion limit for long polylines. + # --------------------------------------------------------------------------- + n = len(pts) + + # keep[i] is True when pts[i] must appear in the output. + keep: list[bool] = [False] * n + keep[0] = True + keep[-1] = True + + # Stack of (start_index, end_index) pairs still to be examined. + stack: list[tuple[int, int]] = [(0, n - 1)] + + while stack: + start, end = stack.pop() + if end - start < 2: + # Only one interior candidate at most; nothing to split further. + continue + + # Find the interior point with the greatest distance to the segment. + max_dist = 0.0 + max_index = start + for i in range(start + 1, end): + dist = _perpendicular_distance(pts[i], pts[start], pts[end]) + if dist > max_dist: + max_dist = dist + max_index = i + + if max_dist > epsilon: + keep[max_index] = True + stack.append((start, max_index)) + stack.append((max_index, end)) + # else: all interior points are within epsilon; discard them all. + + return [pts[i] for i in range(n) if keep[i]] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() From a9f2e72541a30cabf96a4ba459527f4ecb40eca3 Mon Sep 17 00:00:00 2001 From: Sangam Paudel Date: Thu, 21 May 2026 00:01:03 +0545 Subject: [PATCH 04/13] Added Johnson's algorithm for all-pairs shortest paths (#13340) * Fix typos in Johnson's algorithm (nd -> and) to pass codespell * Rename type aliases and h parameter to follow snake_case and descriptive naming * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: John Law Co-authored-by: Christian Clauss Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- graphs/johnson.py | 118 +++++++++++++++++++++++++++++++++++ graphs/tests/test_johnson.py | 24 +++++++ 2 files changed, 142 insertions(+) create mode 100644 graphs/johnson.py create mode 100644 graphs/tests/test_johnson.py diff --git a/graphs/johnson.py b/graphs/johnson.py new file mode 100644 index 000000000000..6306ab5f8654 --- /dev/null +++ b/graphs/johnson.py @@ -0,0 +1,118 @@ +import heapq +from collections.abc import Hashable + +Node = Hashable +edge = tuple[Node, Node, float] +adjacency = dict[Node, list[tuple[Node, float]]] + + +def _collect_nodes_and_edges(graph: adjacency) -> tuple[list[Node], list[edge]]: + nodes = set() + edges: list[edge] = [] + for u, neighbors in graph.items(): + nodes.add(u) + for v, w in neighbors: + nodes.add(v) + edges.append((u, v, w)) + return list(nodes), edges + + +def _bellman_ford(nodes: list[Node], edges: list[edge]) -> dict[Node, float]: + """ + Bellman-Ford relaxation to compute potentials h[v] for all vertices. + Raises ValueError if a negative weight cycle exists. + """ + dist: dict[Node, float] = dict.fromkeys(nodes, 0.0) + n = len(nodes) + + for _ in range(n - 1): + updated = False + for u, v, w in edges: + if dist[u] + w < dist[v]: + dist[v] = dist[u] + w + updated = True + if not updated: + break + else: + for u, v, w in edges: + if dist[u] + w < dist[v]: + raise ValueError("Negative weight cycle detected") + return dist + + +def _dijkstra( + start: Node, + nodes: list[Node], + graph: adjacency, + potentials: dict[Node, float], +) -> dict[Node, float]: + """ + Dijkstra over reweighted graph, using potentials h to make weights non-negative. + Returns distances from start in the reweighted space. + """ + inf = float("inf") + dist: dict[Node, float] = dict.fromkeys(nodes, inf) + dist[start] = 0.0 + heap: list[tuple[float, Node]] = [(0.0, start)] + + while heap: + d_u, u = heapq.heappop(heap) + if d_u > dist[u]: + continue + for v, w in graph.get(u, []): + w_prime = w + potentials[u] - potentials[v] + if w_prime < 0: + raise ValueError( + "Negative edge weight after reweighting: numeric error" + ) + new_dist = d_u + w_prime + if new_dist < dist[v]: + dist[v] = new_dist + heapq.heappush(heap, (new_dist, v)) + return dist + + +def johnson(graph: adjacency) -> dict[Node, dict[Node, float]]: + """ + Compute all-pairs shortest paths using Johnson's algorithm. + + Reference: + https://en.wikipedia.org/wiki/Johnson%27s_algorithm + + Args: + graph: adjacency list {u: [(v, weight), ...], ...} + + Returns: + dict of dicts: dist[u][v] = shortest distance from u to v + + Raises: + ValueError: if a negative weight cycle is detected + + Example: + >>> g = { + ... 0: [(1, 3), (2, 8), (4, -4)], + ... 1: [(3, 1), (4, 7)], + ... 2: [(1, 4)], + ... 3: [(0, 2), (2, -5)], + ... 4: [(3, 6)], + ... } + >>> round(johnson(g)[0][3], 2) + 2.0 + """ + nodes, edges = _collect_nodes_and_edges(graph) + potentials = _bellman_ford(nodes, edges) + + all_pairs: dict[Node, dict[Node, float]] = {} + inf = float("inf") + for s in nodes: + dist_reweighted = _dijkstra(s, nodes, graph, potentials) + dists_orig: dict[Node, float] = {} + for v in nodes: + d_prime = dist_reweighted[v] + if d_prime < inf: + dists_orig[v] = d_prime - potentials[s] + potentials[v] + else: + dists_orig[v] = inf + all_pairs[s] = dists_orig + + return all_pairs diff --git a/graphs/tests/test_johnson.py b/graphs/tests/test_johnson.py new file mode 100644 index 000000000000..e149aac85d0f --- /dev/null +++ b/graphs/tests/test_johnson.py @@ -0,0 +1,24 @@ +import math + +import pytest + +from graphs.johnson import johnson + + +def test_johnson_basic(): + g = { + 0: [(1, 3), (2, 8), (4, -4)], + 1: [(3, 1), (4, 7)], + 2: [(1, 4)], + 3: [(0, 2), (2, -5)], + 4: [(3, 6)], + } + dist = johnson(g) + assert math.isclose(dist[0][3], 2.0, abs_tol=1e-9) + assert math.isclose(dist[3][2], -5.0, abs_tol=1e-9) + + +def test_johnson_negative_cycle(): + g2 = {0: [(1, 1)], 1: [(0, -3)]} + with pytest.raises(ValueError): + johnson(g2) From 456d644c23e17502443b9e6dabb669078e9a895e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 13:51:30 +0200 Subject: [PATCH 05/13] [pre-commit.ci] pre-commit autoupdate (#14629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.9 → v0.15.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.9...v0.15.12) - [github.com/tox-dev/pyproject-fmt: v2.21.0 → v2.21.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.21.0...v2.21.1) * updating DIRECTORY.md * Update pre-commit hook versions --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Christian Clauss Co-authored-by: cclauss --- .pre-commit-config.yaml | 4 ++-- DIRECTORY.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 39daf3dd7f88..adca030fefe0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.9 + rev: v0.15.14 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.21.0 + rev: v2.21.2 hooks: - id: pyproject-fmt diff --git a/DIRECTORY.md b/DIRECTORY.md index ca454bd5fd82..daf71bab8162 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -471,6 +471,8 @@ * [Geometry](geometry/geometry.py) * [Graham Scan](geometry/graham_scan.py) * [Jarvis March](geometry/jarvis_march.py) + * [Ramer Douglas Peucker](geometry/ramer_douglas_peucker.py) + * [Segment Intersection](geometry/segment_intersection.py) * Tests * [Test Graham Scan](geometry/tests/test_graham_scan.py) * [Test Jarvis March](geometry/tests/test_jarvis_march.py) @@ -523,6 +525,7 @@ * [Graphs Floyd Warshall](graphs/graphs_floyd_warshall.py) * [Greedy Best First](graphs/greedy_best_first.py) * [Greedy Min Vertex Cover](graphs/greedy_min_vertex_cover.py) + * [Johnson](graphs/johnson.py) * [Kahns Algorithm Long](graphs/kahns_algorithm_long.py) * [Kahns Algorithm Topo](graphs/kahns_algorithm_topo.py) * [Karger](graphs/karger.py) @@ -543,6 +546,7 @@ * [Strongly Connected Components](graphs/strongly_connected_components.py) * [Tarjans Scc](graphs/tarjans_scc.py) * Tests + * [Test Johnson](graphs/tests/test_johnson.py) * [Test Min Spanning Tree Kruskal](graphs/tests/test_min_spanning_tree_kruskal.py) * [Test Min Spanning Tree Prim](graphs/tests/test_min_spanning_tree_prim.py) From 6c0462028f547fc905a4d9a8cc956daed8a00cd8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:53:38 +0200 Subject: [PATCH 06/13] [pre-commit.ci] pre-commit autoupdate (#14747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.14 → v0.15.15](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.14...v0.15.15) - [github.com/tox-dev/pyproject-fmt: v2.21.2 → v2.23.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.21.2...v2.23.0) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- pyproject.toml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index adca030fefe0..b865f88350ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.14 + rev: v0.15.15 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.21.2 + rev: v2.23.0 hooks: - id: pyproject-fmt diff --git a/pyproject.toml b/pyproject.toml index 34e099a46435..0b0c3f5cfda4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -169,14 +169,14 @@ skip = """\ python_version = "3.14" [tool.pytest] -ini_options.markers = [ - "mat_ops: mark a test as utilizing matrix operations.", -] ini_options.addopts = [ "--durations=10", "--doctest-modules", "--showlocals", ] +ini_options.markers = [ + "mat_ops: mark a test as utilizing matrix operations.", +] [tool.coverage] report.omit = [ From e3b01ecd1267d39d49b99db9676a653ff197db62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 06:00:40 +0200 Subject: [PATCH 07/13] Bump actions/checkout from 6 to 7 (#14820) Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/devcontainer_ci.yml | 2 +- .github/workflows/directory_writer.yml | 2 +- .github/workflows/project_euler.yml | 4 ++-- .github/workflows/ruff.yml | 2 +- .github/workflows/sphinx.yml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2bb8e1d69217..62b62d0f6481 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - run: sudo apt-get update && sudo apt-get install -y libhdf5-dev - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v7 with: enable-cache: true diff --git a/.github/workflows/devcontainer_ci.yml b/.github/workflows/devcontainer_ci.yml index d1b81593866f..e8cd040c1323 100644 --- a/.github/workflows/devcontainer_ci.yml +++ b/.github/workflows/devcontainer_ci.yml @@ -12,7 +12,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: devcontainers/ci@v0.3 with: push: never diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml index deffbe9e364f..598cdfa5a371 100644 --- a/.github/workflows/directory_writer.yml +++ b/.github/workflows/directory_writer.yml @@ -6,7 +6,7 @@ jobs: directory_writer: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: actions/setup-python@v6 diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml index 591b2163cc1a..ff61367974f3 100644 --- a/.github/workflows/project_euler.yml +++ b/.github/workflows/project_euler.yml @@ -21,7 +21,7 @@ jobs: libxml2-dev libxslt-dev libhdf5-dev libopenblas-dev - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v7 - uses: actions/setup-python@v6 with: @@ -39,7 +39,7 @@ jobs: libxml2-dev libxslt-dev libhdf5-dev libopenblas-dev - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v7 - uses: actions/setup-python@v6 with: diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 13df19c8d743..d665d0fb9266 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -11,6 +11,6 @@ jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v7 - run: uvx ruff check --output-format=github . diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 3f00094e0264..7a8fc9460446 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -32,7 +32,7 @@ jobs: libxml2-dev libxslt-dev libhdf5-dev libopenblas-dev - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v7 - uses: actions/setup-python@v6 with: From c0db072a1323339e0d9148479f8818a1b9768d88 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:04:35 +0200 Subject: [PATCH 08/13] [pre-commit.ci] pre-commit autoupdate (#14906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.15 → v0.15.20](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.15...v0.15.20) - [github.com/tox-dev/pyproject-fmt: v2.23.0 → v2.25.1](https://github.com/tox-dev/pyproject-fmt/compare/v2.23.0...v2.25.1) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b865f88350ea..cc52ede8cc08 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.15 + rev: v0.15.20 hooks: - id: ruff-check - id: ruff-format @@ -32,7 +32,7 @@ repos: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.23.0 + rev: v2.25.1 hooks: - id: pyproject-fmt diff --git a/pyproject.toml b/pyproject.toml index 0b0c3f5cfda4..1a55feda3cbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -265,5 +265,5 @@ myst_fence_as_directive = [ ] templates_path = [ "_templates" ] source_suffix.".rst" = "restructuredtext" -# ".txt" = "markdown" source_suffix.".md" = "markdown" +# ".txt" = "markdown" From 25bcced0f37df781b0e5aaa575e2a4fad41a2816 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:13:38 -0400 Subject: [PATCH 09/13] Bump actions/setup-python from 6 to 7 (#14965) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/directory_writer.yml | 2 +- .github/workflows/project_euler.yml | 4 ++-- .github/workflows/sphinx.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62b62d0f6481..594369c6dc7b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: with: enable-cache: true cache-dependency-glob: uv.lock - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: 3.14 allow-prereleases: true diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml index 598cdfa5a371..8b34f97995a7 100644 --- a/.github/workflows/directory_writer.yml +++ b/.github/workflows/directory_writer.yml @@ -9,7 +9,7 @@ jobs: - uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: 3.14 allow-prereleases: true diff --git a/.github/workflows/project_euler.yml b/.github/workflows/project_euler.yml index ff61367974f3..73afb2e71cab 100644 --- a/.github/workflows/project_euler.yml +++ b/.github/workflows/project_euler.yml @@ -23,7 +23,7 @@ jobs: libopenblas-dev - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v7 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: 3.14 allow-prereleases: true @@ -41,7 +41,7 @@ jobs: libopenblas-dev - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v7 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: 3.14 allow-prereleases: true diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index 7a8fc9460446..45a74099ff00 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -34,7 +34,7 @@ jobs: libopenblas-dev - uses: actions/checkout@v7 - uses: astral-sh/setup-uv@v7 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: 3.14 allow-prereleases: true From 948d4cb01e2d8533447e3c0947c757173ec2a87f Mon Sep 17 00:00:00 2001 From: Ali Satwat Khan Date: Fri, 31 Jul 2026 05:29:25 +0500 Subject: [PATCH 10/13] Improve docstrings in sorts/bubble_sort.py (#14924) * docs: expand bubble sort docstrings with algorithm explanation Add a concise description of how bubble sort works (repeated adjacent comparisons/swaps until a pass makes no swaps) and note time/space complexity for both the iterative and recursive implementations. No behavior changes; all existing doctests pass. * Fix Ruff 0.16 lint failures * S310 --------- Co-authored-by: Christian Clauss --- .../inorder_tree_traversal_2022.py | 2 +- sorts/bubble_sort.py | 27 +++++++++++++++++-- .../download_images_from_google_query.py | 7 +++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/data_structures/binary_tree/inorder_tree_traversal_2022.py b/data_structures/binary_tree/inorder_tree_traversal_2022.py index 1357527d2953..a4846a5c29a5 100644 --- a/data_structures/binary_tree/inorder_tree_traversal_2022.py +++ b/data_structures/binary_tree/inorder_tree_traversal_2022.py @@ -43,7 +43,7 @@ def insert(node: BinaryTreeNode | None, new_value: int) -> BinaryTreeNode | None return node -def inorder(node: None | BinaryTreeNode) -> list[int]: # if node is None,return +def inorder(node: BinaryTreeNode | None) -> list[int]: # if node is None,return """ >>> inorder(make_tree()) [6, 10, 14, 15, 20, 25, 60] diff --git a/sorts/bubble_sort.py b/sorts/bubble_sort.py index 4d658a4a12e4..c66d5d59dd93 100644 --- a/sorts/bubble_sort.py +++ b/sorts/bubble_sort.py @@ -2,7 +2,18 @@ def bubble_sort_iterative(collection: list[Any]) -> list[Any]: - """Pure implementation of bubble sort algorithm in Python + """Pure implementation of the bubble sort algorithm in Python (iterative). + + Bubble sort works by repeatedly stepping through the collection, + comparing each pair of adjacent elements and swapping them if they + are in the wrong order. This process repeats, with each full pass + "bubbling" the next-largest unsorted element into its correct + position at the end of the collection, until a full pass completes + with no swaps, at which point the collection is sorted. + + Time complexity: O(n) best case (already sorted, thanks to the + early-exit optimization), O(n^2) average and worst case. + Space complexity: O(1) auxiliary (sorts in place). :param collection: some mutable ordered collection with heterogeneous comparable items inside @@ -61,7 +72,19 @@ def bubble_sort_iterative(collection: list[Any]) -> list[Any]: def bubble_sort_recursive(collection: list[Any]) -> list[Any]: - """It is similar iterative bubble sort but recursive. + """Pure implementation of the bubble sort algorithm in Python (recursive). + + Functionally identical to the iterative version: each call makes a + single pass through the collection, comparing adjacent elements and + swapping any pair that is out of order. If any swap occurred during + the pass, the function calls itself again on the (partially sorted) + collection; once a pass completes with no swaps, the collection is + sorted and the recursion stops. + + Time complexity: O(n) best case (already sorted), O(n^2) average and + worst case. + Space complexity: O(1) auxiliary for the sort itself (sorts in place), + though the recursion adds O(n) call-stack frames in the worst case. :param collection: mutable ordered sequence of elements :return: the same list in ascending order diff --git a/web_programming/download_images_from_google_query.py b/web_programming/download_images_from_google_query.py index 659cf6a398a3..940cc3a5f024 100644 --- a/web_programming/download_images_from_google_query.py +++ b/web_programming/download_images_from_google_query.py @@ -88,8 +88,11 @@ def download_images_from_google_query(query: str = "dhaka", max_images: int = 5) opener.addheaders = [ ( "User-Agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" - " (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582", + ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + " (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36" + " Edge/18.19582" + ), ) ] urllib.request.install_opener(opener) From 758d487394677c2c7e4c468776e066eafe3883a7 Mon Sep 17 00:00:00 2001 From: hojen2 Date: Fri, 31 Jul 2026 08:37:49 -0700 Subject: [PATCH 11/13] Upgrade ruff in pre-commit (#14982) * Fix invalid Python 2 syntax in except clause * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Update ruff-pre-commit version to v0.16.1 * Fix indentation in test_cancer_data function * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Christian Clauss --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc52ede8cc08..8721f6e5b99a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - id: auto-walrus - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.20 + rev: v0.16.1 hooks: - id: ruff-check - id: ruff-format From eea1bacfe0294b82c229432c82a09d3396e8a50c Mon Sep 17 00:00:00 2001 From: Ali Satwat Khan Date: Fri, 31 Jul 2026 21:07:20 +0500 Subject: [PATCH 12/13] fix: raise ValueError in encode() for non-lowercase input (#14936) * fix: raise ValueError in encode() for non-lowercase input encode() previously accepted uppercase letters, digits, and other non-lowercase characters silently, producing incorrect/out-of-range values (e.g. negative numbers for uppercase letters) instead of failing. Add input validation using str.islower() and str.isalpha() to raise a ValueError when the input isn't purely lowercase a-z. Added a doctest covering the new error case. * resolved doctest * Fix Ruff 0.16 lint failures * Enhance encode function error handling examples Update error handling in encode function to include examples for mixed case and invalid characters. * Fix indentation in test_cancer_data function * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Christian Clauss Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- ciphers/a1z26.py | 14 ++++++++++++++ .../sequential_minimum_optimization.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/ciphers/a1z26.py b/ciphers/a1z26.py index a1377ea6d397..d99efe186519 100644 --- a/ciphers/a1z26.py +++ b/ciphers/a1z26.py @@ -13,7 +13,21 @@ def encode(plain: str) -> list[int]: """ >>> encode("myname") [13, 25, 14, 1, 13, 5] + >>> encode("abCd") + Traceback (most recent call last): + ... + ValueError: plain must contain only lowercase letters (a-z) + >>> encode("n0w") + Traceback (most recent call last): + ... + ValueError: plain must contain only lowercase letters (a-z) + >>> encode("later!") + Traceback (most recent call last): + ... + ValueError: plain must contain only lowercase letters (a-z) """ + if not plain.islower() or not plain.isalpha(): + raise ValueError("plain must contain only lowercase letters (a-z)") return [ord(elem) - 96 for elem in plain] diff --git a/machine_learning/sequential_minimum_optimization.py b/machine_learning/sequential_minimum_optimization.py index 625fc28fe60c..e96f06d6f080 100644 --- a/machine_learning/sequential_minimum_optimization.py +++ b/machine_learning/sequential_minimum_optimization.py @@ -451,7 +451,7 @@ def test_cancer_data(): print("Hello!\nStart test SVM using the SMO algorithm!") # 0: download dataset and load into pandas' dataframe if not os.path.exists(r"cancer_data.csv"): - request = urllib.request.Request( # noqa: S310 + request = urllib.request.Request( CANCER_DATASET_URL, headers={"User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)"}, ) From f5988cc09713315817df6a7e327e258013a94440 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:44:36 +0200 Subject: [PATCH 13/13] [pre-commit.ci] pre-commit autoupdate (#14993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/codespell-project/codespell: v2.4.2 → v2.4.3](https://github.com/codespell-project/codespell/compare/v2.4.2...v2.4.3) - [github.com/tox-dev/pyproject-fmt: v2.25.1 → v2.26.0](https://github.com/tox-dev/pyproject-fmt/compare/v2.25.1...v2.26.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8721f6e5b99a..0a5f50751ce3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,14 +25,14 @@ repos: - id: ruff-format - repo: https://github.com/codespell-project/codespell - rev: v2.4.2 + rev: v2.4.3 hooks: - id: codespell additional_dependencies: - tomli - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.25.1 + rev: v2.26.0 hooks: - id: pyproject-fmt