Skip to content

Conversation

@BrianLusina
Copy link
Owner

@BrianLusina BrianLusina commented Dec 5, 2023

Vertex & Graph implementations

  • Add an algorithm?
  • Fix a bug or typo in an existing algorithm?
  • Documentation change?

Checklist:

  • I have read CONTRIBUTING.md.
  • This pull request is all my own work -- I have not plagiarized.
  • I know that pull requests will not be merged if they fail the automated tests.
  • This PR only changes one algorithm file. To ease review, please open separate PRs for separate algorithms.
  • All new Python files are placed inside an existing directory.
  • All filenames are in all lowercase characters with no spaces or dashes.
  • All functions and variable names follow Python naming conventions.
  • All function parameters and return values are annotated with Python type hints.

Summary by CodeRabbit

  • New Features

    • Added specialized edge types for various graph structures.
    • Introduced undirected graph implementation with BFS operations.
    • Added utility function for finding closest nodes in tree structures based on distance metrics.
    • Enhanced vertex functionality with improved adjacency tracking.
  • Documentation

    • Added documentation for tree path analysis problems.
  • Tests

    • Added test coverage for vertex operations and tree utilities.

✏️ Tip: You can customize this high-level summary in your review settings.

@BrianLusina BrianLusina added Algorithm Algorithm Problem Datastructures Datastructures Documentation Documentation Updates Graph Graph data structures and algorithms labels Dec 5, 2023
@BrianLusina BrianLusina self-assigned this Dec 5, 2023
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 24, 2025

Walkthrough

This pull request refactors the graph data structure module by extracting the monolithic Edge class into an abstract base with five specialized subclasses (DirectedEdge, UndirectedEdge, SelfEdge, HyperEdge) organized in a new edge/ subpackage. The Graph class is introduced with pathfinding and traversal methods, Vertex is updated with adjacency tracking, and new undirected graph utilities are added. The module is restructured as a facade pattern.

Changes

Cohort / File(s) Summary
Documentation & Organization
DIRECTORY.md, datastructures/graphs/undirected/README.md
DIRECTORY.md reorganizes Graphs and Trees sections into hierarchical substructures with new test/document links. Undirected graph README documents a "Closest Node to Path in Tree" problem with examples.
Module Restructuring & Exports
datastructures/graphs/__init__.py, datastructures/graphs/undirected/__init__.py
Both modules converted to facade pattern: main __init__.py now re-exports Edge types and Graph from submodules via __all__; undirected __init__.py re-exports UndirectedGraph from submodule. Previous local class definitions removed.
Edge Type Hierarchy
datastructures/graphs/edge/edge_type.py, datastructures/graphs/edge/edge.py
New EdgeType enum with members UNDIRECTED, DIRECTED, SELF, HYPER_DIRECTED, HYPER_UNDIRECTED. New abstract generic Edge[T] base class with weight, properties, identifier; abstract methods edge_type() and vertices(); concrete method is_unweighted().
Edge Subclasses
datastructures/graphs/edge/edge_directed.py, datastructures/graphs/edge/edge_undirected.py, datastructures/graphs/edge/edge_self.py, datastructures/graphs/edge/edge_hyper.py
Four new concrete edge implementations extending abstract Edge: DirectedEdge (source/destination pair), UndirectedEdge (two nodes), SelfEdge (single node), HyperEdge (multiple nodes). Each returns appropriate edge_type() and implements vertices() and __str__().
Edge Package Export
datastructures/graphs/edge/__init__.py
Centralized imports from submodules with explicit __all__ export of Edge, EdgeType, DirectedEdge, UndirectedEdge, SelfEdge, HyperEdge.
Legacy Edge Removal
datastructures/graphs/edge.py
File removed; concrete Edge class definition eliminated and distributed across new subpackage hierarchy.
Core Graph Implementation
datastructures/graphs/graph.py
New generic Graph[T] class using adjacency list; supports edge addition, adjacency construction, topological sort (DFS with cycle detection), BFS (abstract), pathfinding (recursive), node removal, connectivity checks, and pretty-printing.
Vertex Enhancement
datastructures/graphs/vertex.py
Added adjacent_vertices dict, __eq__() method comparing by id, add_adjacent_vertex() method for undirected adjacency tracking. Constructor signature reordered; degree calculations updated to use edge node attributes.
Undirected Graph
datastructures/graphs/undirected/undirected_graph.py
New UndirectedGraph class extending Graph, forwards edge_list to base, declares stub BFS methods (unimplemented).
Graph Utilities
datastructures/graphs/undirected/utils.py
New closest_node() function: builds adjacency list, performs BFS-based distance computation from target node, reconstructs paths, selects closest node on path (tie-breaking by smallest index).
Tests
datastructures/graphs/test_vertex.py, datastructures/graphs/undirected/test_utils.py
New test files: test_vertex.py defines placeholder VertexTestCases suite; test_utils.py adds parameterized ClosestNodeToPathInTreeTestCase covering multiple scenarios for closest_node() function.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • datastructures/graphs/edge/edge.py — Abstract base class design and generic type variable usage; verify all subclasses properly implement abstract methods.
  • datastructures/graphs/graph.py — Review adjacency list construction, topological sort cycle detection logic, and recursive pathfinding implementations (find_path, find_all_paths, find_shortest_path) for correctness and edge cases.
  • datastructures/graphs/vertex.py — Verify degree calculations after refactoring (in_degree/out_degree now check node_two/node_one on edges); ensure adjacent_vertices tracking doesn't create inconsistencies with edge lists.
  • datastructures/graphs/undirected/utils.py — Validate BFS-based distance computation and path reconstruction logic in closest_node(), particularly the tie-breaking behavior.
  • Module facade pattern — Confirm all re-exports in __all__ are complete and that removed local definitions don't break downstream imports.

Poem

🐰 Our edges scatter into five, refactored clean,
from tangled class to types pristine—
DirectedEdge, Undirected, Self, and Hyper too,
Abstract base blooms; facades shine anew!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is largely incomplete. Multiple required checklist items are unchecked (doctests, algorithm URLs, issue references), which represent critical missing components per the template. Complete unchecked items: add doctests for all new functions, include Wikipedia/reference URLs in code comments, and if fixing issues, add 'Fixes: #{ISSUE_NO}' to commit message.
Docstring Coverage ⚠️ Warning Docstring coverage is 55.42% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Graphs | Vertex & Graph implementations' directly and specifically describes the main changes: introducing new Vertex and Graph implementations in the graphs module.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/graphs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Oct 24, 2025

Warnings
⚠️ ❗ Big PR

: Pull Request size seems relatively large. If Pull Request contains multiple changes, split each into separate PR will helps faster, easier review.

Generated by 🚫 dangerJS against 0730fe9

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
datastructures/graphs/vertex.py (2)

72-91: Degree calculation is incorrect.

  • Early return triggers when either incoming or outgoing is empty; should be when both are empty.
  • Use Edge.is_unweighted() per API; don’t treat weight 0 as unweighted.
-        degrees = 0
-
-        if len(self.incoming_edges) == 0 or len(self.outgoing_edges) == 0:
-            return degrees
-
-        seen_edges: Set = set()
-
-        for edge in self.edges:
-            if edge not in seen_edges:
-                seen_edges.add(edge)
-                if not edge.weight:
-                    degrees += 1
-
-        return degrees
+        if not self.edges:
+            return 0
+        return sum(1 for edge in self.edges if edge.is_unweighted())

101-109: Directed-degree checks reference non-existent attributes.

Use DirectedEdge and its source/destination fields (or edge.edge_type()). Current code uses edge.type and node_one/node_two, which don’t exist for DirectedEdge.

-        if len(self.edges) == 0:
+        if not self.edges:
             return in_degrees
-
-        for edge in self.edges:
-            if edge.type == EdgeType.DIRECTED and edge.node_two == self:
-                in_degrees += 1
+        for edge in self.edges:
+            if isinstance(edge, DirectedEdge) and edge.destination == self:
+                in_degrees += 1
@@
-        if len(self.edges) == 0:
+        if not self.edges:
             return out_degrees
-
-        for edge in self.edges:
-            if edge.type == EdgeType.DIRECTED and edge.node_one == self:
-                out_degrees += 1
+        for edge in self.edges:
+            if isinstance(edge, DirectedEdge) and edge.source == self:
+                out_degrees += 1

Also applies to: 123-127

🧹 Nitpick comments (7)
datastructures/graphs/vertex.py (1)

58-69: Minor: simplify neighbours.

No behavior change.

-        nodes = []
-        for vertex in self.adjacent_vertices.values():
-            nodes.append(vertex)
-
-        return nodes
+        return list(self.adjacent_vertices.values())
datastructures/graphs/edge/edge_type.py (1)

4-10: Enum looks good; consider auto() and future-proofing.

Current members are fine. Optional: use auto() to avoid manual numbering and reduce churn when adding new types. If HyperEdge won’t encode direction, introduce a generic HYPER member and align uses accordingly.

 from enum import Enum, unique
 
 @unique
 class EdgeType(Enum):
-    UNDIRECTED = 1
-    DIRECTED = 2
-    SELF = 3
-    HYPER_DIRECTED = 4
-    HYPER_UNDIRECTED = 5
+    UNDIRECTED = auto()
+    DIRECTED = auto()
+    SELF = auto()
+    HYPER_DIRECTED = auto()
+    HYPER_UNDIRECTED = auto()
datastructures/graphs/edge/edge_hyper.py (1)

13-26: Tighten generics: use List[T] where possible.

HyperEdge is Generic[T] but currently uses List[Any]. Prefer List[T] for nodes and return type.

-    def __init__(self, nodes: List[Any], ...
+    def __init__(self, nodes: List[T], ...
 ...
-    def vertices(self) -> List[Any]:
+    def vertices(self) -> List[T]:
         return self.nodes
datastructures/graphs/edge/edge_self.py (1)

19-26: Minor: property name clarity.

For a single node, consider self.node over self.node_one for clarity.

-        self.node_one = node
+        self.node = node
 ...
-        return f"{super().__str__()}, Node: {self.node_one}"
+        return f"{super().__str__()}, Node: {self.node}"
 ...
-    def vertices(self) -> List[Any]:
-        return [self.node_one]
+    def vertices(self) -> List[Any]:
+        return [self.node]
datastructures/graphs/edge/edge_directed.py (1)

27-28: Tighten generics: return List[T].

Since the edge is Generic[T], return a typed list to improve correctness.

-    def vertices(self) -> List[Any]:
+    def vertices(self) -> List[T]:
         return [self.source, self.destination]
datastructures/graphs/edge/edge_undirected.py (1)

8-11: Optional: Remove redundant Generic inheritance.

Since Edge already inherits from Generic[T], explicitly inheriting from Generic[T] again in UndirectedEdge is redundant. While not incorrect, it can be simplified.

Apply this diff to simplify the inheritance:

-class UndirectedEdge(Edge, Generic[T]):
+class UndirectedEdge(Edge[T]):
datastructures/graphs/edge/edge.py (1)

1-7: Optional: Consolidate typing imports for better organization.

The typing imports are split across lines 2 and 4, which slightly reduces readability.

Apply this diff to consolidate the imports:

 from abc import ABC, abstractmethod
-from typing import AnyStr, Union
+from typing import AnyStr, Union, Any, Dict, Optional, Generic, TypeVar, List
 from .edge_type import EdgeType
-from typing import Any, Dict, Optional, Generic, TypeVar, List
 from uuid import uuid4
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c5ac87 and 69d657a.

📒 Files selected for processing (13)
  • DIRECTORY.md (1 hunks)
  • datastructures/graphs/__init__.py (1 hunks)
  • datastructures/graphs/edge.py (0 hunks)
  • datastructures/graphs/edge/__init__.py (1 hunks)
  • datastructures/graphs/edge/edge.py (1 hunks)
  • datastructures/graphs/edge/edge_directed.py (1 hunks)
  • datastructures/graphs/edge/edge_hyper.py (1 hunks)
  • datastructures/graphs/edge/edge_self.py (1 hunks)
  • datastructures/graphs/edge/edge_type.py (1 hunks)
  • datastructures/graphs/edge/edge_undirected.py (1 hunks)
  • datastructures/graphs/graph.py (1 hunks)
  • datastructures/graphs/test_vertex.py (1 hunks)
  • datastructures/graphs/vertex.py (5 hunks)
💤 Files with no reviewable changes (1)
  • datastructures/graphs/edge.py
🧰 Additional context used
🧬 Code graph analysis (10)
datastructures/graphs/edge/edge_self.py (5)
datastructures/graphs/edge/edge.py (3)
  • edge_type (28-29)
  • Edge (9-36)
  • vertices (35-36)
datastructures/graphs/edge/edge_directed.py (2)
  • edge_type (24-25)
  • vertices (27-28)
datastructures/graphs/edge/edge_hyper.py (2)
  • edge_type (22-23)
  • vertices (25-26)
datastructures/graphs/edge/edge_undirected.py (2)
  • edge_type (23-24)
  • vertices (26-27)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
datastructures/graphs/edge/__init__.py (6)
datastructures/graphs/edge/edge.py (2)
  • Edge (9-36)
  • edge_type (28-29)
datastructures/graphs/edge/edge_directed.py (2)
  • edge_type (24-25)
  • DirectedEdge (8-28)
datastructures/graphs/edge/edge_hyper.py (2)
  • edge_type (22-23)
  • HyperEdge (8-26)
datastructures/graphs/edge/edge_self.py (2)
  • edge_type (22-23)
  • SelfEdge (8-26)
datastructures/graphs/edge/edge_undirected.py (2)
  • edge_type (23-24)
  • UndirectedEdge (8-27)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
datastructures/graphs/edge/edge.py (5)
datastructures/graphs/edge/edge_directed.py (2)
  • edge_type (24-25)
  • vertices (27-28)
datastructures/graphs/edge/edge_hyper.py (2)
  • edge_type (22-23)
  • vertices (25-26)
datastructures/graphs/edge/edge_self.py (2)
  • edge_type (22-23)
  • vertices (25-26)
datastructures/graphs/edge/edge_undirected.py (2)
  • edge_type (23-24)
  • vertices (26-27)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
datastructures/graphs/graph.py (3)
datastructures/stacks/__init__.py (1)
  • Stack (7-48)
datastructures/graphs/vertex.py (2)
  • Vertex (8-127)
  • neighbours (59-69)
datastructures/graphs/edge/edge.py (1)
  • Edge (9-36)
datastructures/graphs/edge/edge_hyper.py (5)
datastructures/graphs/edge/edge.py (3)
  • edge_type (28-29)
  • Edge (9-36)
  • vertices (35-36)
datastructures/graphs/edge/edge_directed.py (2)
  • edge_type (24-25)
  • vertices (27-28)
datastructures/graphs/edge/edge_self.py (2)
  • edge_type (22-23)
  • vertices (25-26)
datastructures/graphs/edge/edge_undirected.py (2)
  • edge_type (23-24)
  • vertices (26-27)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
datastructures/graphs/test_vertex.py (2)
datastructures/graphs/vertex.py (1)
  • Vertex (8-127)
datastructures/graphs/edge/edge_undirected.py (1)
  • UndirectedEdge (8-27)
datastructures/graphs/edge/edge_directed.py (2)
datastructures/graphs/edge/edge.py (3)
  • edge_type (28-29)
  • Edge (9-36)
  • vertices (35-36)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
datastructures/graphs/__init__.py (7)
datastructures/graphs/edge/edge.py (1)
  • Edge (9-36)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
datastructures/graphs/edge/edge_directed.py (1)
  • DirectedEdge (8-28)
datastructures/graphs/edge/edge_undirected.py (1)
  • UndirectedEdge (8-27)
datastructures/graphs/edge/edge_self.py (1)
  • SelfEdge (8-26)
datastructures/graphs/edge/edge_hyper.py (1)
  • HyperEdge (8-26)
datastructures/graphs/graph.py (1)
  • Graph (12-230)
datastructures/graphs/edge/edge_undirected.py (5)
datastructures/graphs/edge/edge.py (3)
  • edge_type (28-29)
  • Edge (9-36)
  • vertices (35-36)
datastructures/graphs/edge/edge_directed.py (2)
  • edge_type (24-25)
  • vertices (27-28)
datastructures/graphs/edge/edge_hyper.py (2)
  • edge_type (22-23)
  • vertices (25-26)
datastructures/graphs/edge/edge_self.py (2)
  • edge_type (22-23)
  • vertices (25-26)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
datastructures/graphs/vertex.py (2)
datastructures/graphs/edge/edge.py (1)
  • Edge (9-36)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
🪛 markdownlint-cli2 (0.18.1)
DIRECTORY.md

198-198: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


199-199: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


200-200: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


201-201: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


202-202: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


203-203: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


204-204: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


205-205: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


206-206: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🔇 Additional comments (8)
datastructures/graphs/__init__.py (1)

1-14: Public API facade looks good.

Re-exports are clear and usable. No action needed.

datastructures/graphs/edge/__init__.py (1)

1-6: Public exports OK; verify enum usage across modules.

Re-exports look correct. Cross-check edge_undirected.py: snippet shows EdgeType.Undirected (wrong case). Should be EdgeType.UNDIRECTED or code will break.

Suggested fix in datastructures/graphs/edge/edge_undirected.py:

-    def edge_type(self) -> EdgeType:
-        return EdgeType.Undirected
+    def edge_type(self) -> EdgeType:
+        return EdgeType.UNDIRECTED
datastructures/graphs/edge/edge_undirected.py (3)

1-6: Imports and type variable are set up correctly.

The imports are well-organized and the TypeVar declaration follows best practices for generic classes.


20-21: LGTM!

The string representation correctly extends the base class and includes the undirected edge's node information.


26-27: LGTM!

The vertices method correctly returns both nodes of the undirected edge, consistent with the pattern used in other edge implementations.

datastructures/graphs/edge/edge.py (3)

24-25: LGTM!

The string representation provides clear, informative output for debugging and logging.


27-29: LGTM!

The abstract methods edge_type and vertices correctly define the interface contract that all edge subclasses must implement. The use of @abstractmethod ensures subclasses provide concrete implementations.

Also applies to: 34-36


31-32: LGTM!

The is_unweighted helper method provides a clean API for checking whether an edge has a weight, which is useful for graph algorithms that need to distinguish between weighted and unweighted edges.

Comment on lines 14 to 18
def __init__(self, source: Any, destination: Any, weight: Optional[Union[int, float]] = None,
properties: Optional[Dict[str, Any]] = None,
identifier: AnyStr = uuid4()):
super().__init__(weight, properties, identifier)
self.source = source
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: uuid4() as default arg; use late binding and UUID typing.

Same issue as other edges.

-from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any
-from uuid import uuid4
+from typing import Union, Dict, Optional, Generic, TypeVar, List, Any
+from uuid import uuid4, UUID
 ...
-    def __init__(self, source: Any, destination: Any, weight: Optional[Union[int, float]] = None,
-                 properties: Optional[Dict[str, Any]] = None,
-                 identifier: AnyStr = uuid4()):
-        super().__init__(weight, properties, identifier)
+    def __init__(self, source: Any, destination: Any, weight: Optional[Union[int, float]] = None,
+                 properties: Optional[Dict[str, Any]] = None,
+                 identifier: Optional[UUID] = None):
+        super().__init__(weight, properties, identifier or uuid4())

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In datastructures/graphs/edge/edge_directed.py around lines 14 to 18, the
constructor uses uuid4() as a default argument which causes a single UUID to be
shared across calls and the identifier typing should be a UUID; change the
signature to accept identifier: Optional[UUID] = None (import UUID from uuid or
typing), and inside __init__ set self.identifier = identifier or uuid4(); keep
calling super() with the resolved identifier and ensure imports and typing match
other edge classes for consistency.

Comment on lines 13 to 16
def __init__(self, nodes: List[Any], weight: Optional[Union[int, float]] = None,
properties: Optional[Dict[str, Any]] = None,
identifier: AnyStr = uuid4()):
super().__init__(weight, properties, identifier)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: uuid4() used as a default argument (evaluated at import time).

This gives the same id for all instances created without an explicit identifier.

-from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any
-from uuid import uuid4
+from typing import Union, Dict, Optional, Generic, TypeVar, List, Any
+from uuid import uuid4, UUID
 ...
-    def __init__(self, nodes: List[Any], weight: Optional[Union[int, float]] = None,
-                 properties: Optional[Dict[str, Any]] = None,
-                 identifier: AnyStr = uuid4()):
-        super().__init__(weight, properties, identifier)
+    def __init__(self, nodes: List[Any], weight: Optional[Union[int, float]] = None,
+                 properties: Optional[Dict[str, Any]] = None,
+                 identifier: Optional[UUID] = None):
+        super().__init__(weight, properties, identifier or uuid4())

Also align base Edge to accept Optional[UUID] (see follow‑up note below).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In datastructures/graphs/edge/edge_hyper.py around lines 13 to 16, the
constructor uses uuid4() as a default argument causing the same UUID to be
shared across all instances; change the signature so identifier: Optional[UUID]
= None and inside __init__ set identifier = identifier or uuid4() before calling
super().__init__; also update the base Edge type annotations and signature to
accept Optional[UUID] (not just AnyStr) so the propagated identifier type is
Optional[UUID] and maintain existing behavior when an identifier is provided.

Comment on lines +22 to +23
def edge_type(self) -> EdgeType:
return EdgeType.SELF
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: HyperEdge returns the wrong EdgeType.

edge_type() returns EdgeType.SELF. Use a hyper variant (likely undirected unless you carry direction).

-    def edge_type(self) -> EdgeType:
-        return EdgeType.SELF
+    def edge_type(self) -> EdgeType:
+        return EdgeType.HYPER_UNDIRECTED
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def edge_type(self) -> EdgeType:
return EdgeType.SELF
def edge_type(self) -> EdgeType:
return EdgeType.HYPER_UNDIRECTED
🤖 Prompt for AI Agents
In datastructures/graphs/edge/edge_hyper.py around lines 22-23, the edge_type()
method incorrectly returns EdgeType.SELF; change it to return the hyper variant
(e.g., EdgeType.HYPER or EdgeType.UNDIRECTED_HYPER depending on the enum name
used in the codebase) so HyperEdge reports the correct type; update any imports
or references if necessary and run tests to ensure consumers expecting hyper
edges now see the correct enum value.

Comment on lines 13 to 17
def __init__(self, node: Any, weight: Optional[Union[int, float]] = None,
properties: Optional[Dict[str, Any]] = None,
identifier: AnyStr = uuid4()):
super().__init__(weight, properties, identifier)
self.node_one = node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: uuid4() as default arg; switch to Optional[UUID] and late binding.

Avoid import-time evaluation and align identifier typing to UUID.

-from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any
-from uuid import uuid4
+from typing import Union, Dict, Optional, Generic, TypeVar, List, Any
+from uuid import uuid4, UUID
 ...
-    def __init__(self, node: Any, weight: Optional[Union[int, float]] = None,
-                 properties: Optional[Dict[str, Any]] = None,
-                 identifier: AnyStr = uuid4()):
-        super().__init__(weight, properties, identifier)
+    def __init__(self, node: Any, weight: Optional[Union[int, float]] = None,
+                 properties: Optional[Dict[str, Any]] = None,
+                 identifier: Optional[UUID] = None):
+        super().__init__(weight, properties, identifier or uuid4())
🤖 Prompt for AI Agents
In datastructures/graphs/edge/edge_self.py around lines 13 to 17, the
constructor uses uuid4() as a default argument which evaluates at import time
and the identifier type should be a UUID; change the signature to accept
identifier: Optional[UUID] = None (import UUID from uuid or typing) and in the
body do identifier = uuid4() if identifier is None, then pass that UUID to
super().__init__; also update the type hints and any imports accordingly so
identifier is typed as UUID and default is lazily generated.

Comment on lines 13 to 18
def __init__(self, node_one: Any, node_two: Any, weight: Optional[Union[int, float]] = None,
properties: Optional[Dict[str, Any]] = None,
identifier: AnyStr = uuid4()):
super().__init__(weight, properties, identifier)
self.node_one = node_one
self.node_two = node_two
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Mutable default argument and type mismatch for identifier parameter.

Two issues with the identifier parameter:

  1. Mutable default: uuid4() is called once at function definition time, causing all instances created without an explicit identifier to share the same UUID. This will lead to incorrect edge identification and potential data corruption.

  2. Type mismatch: The parameter is typed as AnyStr (which is str | bytes), but uuid4() returns a UUID object.

Apply this diff to fix both issues:

-    def __init__(self, node_one: Any, node_two: Any, weight: Optional[Union[int, float]] = None,
-                 properties: Optional[Dict[str, Any]] = None,
-                 identifier: AnyStr = uuid4()):
+    def __init__(self, node_one: Any, node_two: Any, weight: Optional[Union[int, float]] = None,
+                 properties: Optional[Dict[str, Any]] = None,
+                 identifier: Optional[Union[str, UUID]] = None):
+        if identifier is None:
+            identifier = uuid4()
         super().__init__(weight, properties, identifier)

Note: The base Edge class has the same issue and should be fixed there as well.

🤖 Prompt for AI Agents
In datastructures/graphs/edge/edge_undirected.py lines 13-18, the constructor
currently uses uuid4() as a default argument and types identifier as AnyStr;
change the signature to accept identifier: Optional[UUID] = None (import UUID
from uuid), and inside __init__ do if identifier is None: identifier = uuid4()
before calling super().__init__; this avoids a single shared UUID and matches
the UUID return type; also make the same change in the base Edge class (use
Optional[UUID] = None and assign uuid4() inside the constructor).

Comment on lines +169 to +196
def find_all_paths(
self, node_one: Vertex, node_two: Vertex, path: List = None
) -> list:
"""
Finds all paths between node_one and node_two, where node_one is the start & node_two is the end
:param node_one Graph Node
:param node_two Graph Node
:param path
"""
if path is None:
path = []
path = path + [node_one]

if node_one.data == node_two.data:
return [path]

if node_one.data not in self.adjacency_list:
return []

paths = []

for node in self.adjacency_list[node_one.data]:
if node not in path:
newpaths = self.find_all_paths(Vertex(node), node_two, path)
for newpath in newpaths:
paths.append(newpath)

return paths
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

find_all_paths(): indexes with node_one.data and constructs new Vertex() erroneously.

-        if path is None:
-            path = []
-        path = path + [node_one]
-
-        if node_one.data == node_two.data:
+        if path is None:
+            path = []
+        path = path + [node_one]
+
+        if node_one == node_two:
             return [path]
-
-        if node_one.data not in self.adjacency_list:
-            return []
-
-        paths = []
-
-        for node in self.adjacency_list[node_one.data]:
-            if node not in path:
-                newpaths = self.find_all_paths(Vertex(node), node_two, path)
-                for newpath in newpaths:
-                    paths.append(newpath)
-
-        return paths
+        paths: List[List[Vertex]] = []
+        for neighbour in self.adjacency_list.get(node_one, []):
+            if neighbour not in path:
+                new_paths = self.find_all_paths(neighbour, node_two, path)
+                paths.extend(new_paths)
+        return paths
🤖 Prompt for AI Agents
In datastructures/graphs/graph.py around lines 169-196, find_all_paths is
indexing the adjacency_list with node_one.data and constructing new Vertex(node)
when recursing; instead, use the actual Vertex objects stored in the
adjacency_list (or consistently map between data->Vertex first) so you don't
rebuild vertices or mix data keys with Vertex keys. Update the function to look
up neighbors with self.adjacency_list.get(node_one) (or translate node_one.data
to the Vertex once at the top), iterate neighbor Vertex instances, compare
neighbor != node (or compare their unique id/identity rather than .data), and
pass the neighbor Vertex directly into the recursive call (no new Vertex(...)).
Ensure adjacency_list usage is consistent (either keys/values are Vertex objects
or you convert data->Vertex once) and adjust membership checks against path to
use Vertex objects.

Comment on lines +198 to +224
def find_shortest_path(
self, node_one: Vertex, node_two: Vertex, path: List = None
) -> Union[List, None]:
"""
Finds the shortest path between 2 nodes in the graph
"""
if path is None:
path = []

path = path + [node_one]

if node_one.data == node_two.data:
return path

if node_one.data not in self.adjacency_list:
return None

shortest = None

for node in self.adjacency_list[node_one]:
if node.data not in path:
newpath = self.find_shortest_path(node, node_two, path)
if newpath:
if not shortest or len(newpath) < len(shortest):
shortest = newpath

return shortest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

find_shortest_path(): same keying issues as above.

-        if path is None:
-            path = []
-
-        path = path + [node_one]
-
-        if node_one.data == node_two.data:
+        if path is None:
+            path = []
+        path = path + [node_one]
+
+        if node_one == node_two:
             return path
-
-        if node_one.data not in self.adjacency_list:
-            return None
-
-        shortest = None
-
-        for node in self.adjacency_list[node_one]:
-            if node.data not in path:
-                newpath = self.find_shortest_path(node, node_two, path)
-                if newpath:
-                    if not shortest or len(newpath) < len(shortest):
-                        shortest = newpath
-
-        return shortest
+        shortest: Union[List[Vertex], None] = None
+        for neighbour in self.adjacency_list.get(node_one, []):
+            if neighbour not in path:
+                new_path = self.find_shortest_path(neighbour, node_two, path)
+                if new_path and (shortest is None or len(new_path) < len(shortest)):
+                    shortest = new_path
+        return shortest
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def find_shortest_path(
self, node_one: Vertex, node_two: Vertex, path: List = None
) -> Union[List, None]:
"""
Finds the shortest path between 2 nodes in the graph
"""
if path is None:
path = []
path = path + [node_one]
if node_one.data == node_two.data:
return path
if node_one.data not in self.adjacency_list:
return None
shortest = None
for node in self.adjacency_list[node_one]:
if node.data not in path:
newpath = self.find_shortest_path(node, node_two, path)
if newpath:
if not shortest or len(newpath) < len(shortest):
shortest = newpath
return shortest
def find_shortest_path(
self, node_one: Vertex, node_two: Vertex, path: List = None
) -> Union[List, None]:
"""
Finds the shortest path between 2 nodes in the graph
"""
if path is None:
path = []
path = path + [node_one]
if node_one == node_two:
return path
shortest: Union[List[Vertex], None] = None
for neighbour in self.adjacency_list.get(node_one, []):
if neighbour not in path:
new_path = self.find_shortest_path(neighbour, node_two, path)
if new_path and (shortest is None or len(new_path) < len(shortest)):
shortest = new_path
return shortest
🤖 Prompt for AI Agents
In datastructures/graphs/graph.py around lines 198-224, the method mixes Vertex
objects and their .data keys (e.g. checking node_one.data in adjacency_list but
indexing self.adjacency_list[node_one], and comparing node.data to items in
path), causing incorrect membership/equality checks; fix by treating vertices
consistently as Vertex objects: check membership with node_one in
self.adjacency_list, compare nodes with equality on the Vertex (e.g. node_one ==
node_two), store Vertex instances in path (no .data), and use node not node.data
when checking if a neighbor is already in path or when recursing/indexing
adjacency_list.

Comment on lines 6 to 16
class VertexTestCases(unittest.TestCase):
def test_1(self):
node_one = Vertex(data=1)
node_two = Vertex(data=2)
edge = UndirectedEdge(node_one=node_one, node_two=node_two)

self.assertEqual(True, False) # add assertion here


if __name__ == '__main__':
unittest.main()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace placeholder failing test with real Vertex checks.

Add adjacency and equality/hash tests.

-class VertexTestCases(unittest.TestCase):
-    def test_1(self):
-        node_one = Vertex(data=1)
-        node_two = Vertex(data=2)
-        edge = UndirectedEdge(node_one=node_one, node_two=node_two)
-
-        self.assertEqual(True, False)  # add assertion here
+class VertexTestCases(unittest.TestCase):
+    def test_add_adjacent_vertex_is_mutual(self):
+        v1 = Vertex(data=1)
+        v2 = Vertex(data=2)
+        v1.add_adjacent_vertex(v2)
+        self.assertIn(v2, v1.neighbours)
+        self.assertIn(v1, v2.neighbours)
+
+    def test_eq_and_hash_by_id(self):
+        v1 = Vertex(data=1)
+        v2 = Vertex(data=99, identifier=v1.id)
+        self.assertEqual(v1, v2)
+        self.assertIn(v2, {v1})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class VertexTestCases(unittest.TestCase):
def test_1(self):
node_one = Vertex(data=1)
node_two = Vertex(data=2)
edge = UndirectedEdge(node_one=node_one, node_two=node_two)
self.assertEqual(True, False) # add assertion here
if __name__ == '__main__':
unittest.main()
class VertexTestCases(unittest.TestCase):
def test_add_adjacent_vertex_is_mutual(self):
v1 = Vertex(data=1)
v2 = Vertex(data=2)
v1.add_adjacent_vertex(v2)
self.assertIn(v2, v1.neighbours)
self.assertIn(v1, v2.neighbours)
def test_eq_and_hash_by_id(self):
v1 = Vertex(data=1)
v2 = Vertex(data=99, identifier=v1.id)
self.assertEqual(v1, v2)
self.assertIn(v2, {v1})
if __name__ == '__main__':
unittest.main()
🤖 Prompt for AI Agents
In datastructures/graphs/test_vertex.py around lines 6 to 16, replace the
failing placeholder assertion with real tests: create two Vertex instances and
an UndirectedEdge between them, then assert that each vertex's
adjacency/container includes the other (and the edge if your API stores edges),
assert that two Vertex instances constructed with the same data compare equal
and produce the same hash, and assert that vertices with different data are not
equal; use assertTrue/assertFalse/assertEqual/assertIn/assertNotIn as
appropriate to validate adjacency and equality/hash behavior.

Comment on lines 13 to 32
def __init__(
self,
data: T,
incoming_edges: Set[Edge],
outgoing_edges: Set[Edge],
properties: Optional[Dict[str, Any]] = None,
identifier: Any = uuid4(),
properties: Optional[Dict[str, Any]] = None,
incoming_edges: Optional[Set[Edge]] = None,
outgoing_edges: Optional[Set[Edge]] = None,
):
if outgoing_edges is None:
outgoing_edges = set()
if incoming_edges is None:
incoming_edges = set()
self.id = identifier
self.data = data
self.incoming_edges = incoming_edges
self.outgoing_edges = outgoing_edges
self.edges = self.incoming_edges.union(self.outgoing_edges)
self.adjacent_vertices: Dict[str, 'Vertex'] = {}
self.properties = properties

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Default-id bug, stale edges, and key typing. Make Vertex hashable.

  • Avoid uuid4() as a default arg (same id across instances).
  • Don’t cache edges; compute union on demand.
  • adjacent_vertices key type should match id type.
  • Add hash to use Vertex as dict key (Graph uses it).
-from .edge import Edge, EdgeType
+from .edge import Edge, EdgeType, DirectedEdge
@@
-    def __init__(
+    def __init__(
         self,
         data: T,
-        identifier: Any = uuid4(),
+        identifier: Any = None,
         properties: Optional[Dict[str, Any]] = None,
         incoming_edges: Optional[Set[Edge]] = None,
         outgoing_edges: Optional[Set[Edge]] = None,
     ):
@@
-        self.id = identifier
+        self.id = uuid4() if identifier is None else identifier
         self.data = data
@@
-        self.edges = self.incoming_edges.union(self.outgoing_edges)
-        self.adjacent_vertices: Dict[str, 'Vertex'] = {}
+        self.adjacent_vertices: Dict[Any, 'Vertex'] = {}
         self.properties = properties
+
+    @property
+    def edges(self) -> Set[Edge]:
+        return self.incoming_edges | self.outgoing_edges
@@
-    def __eq__(self, other: "Vertex") -> bool:
-        return self.id == other.id
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, Vertex):
+            return NotImplemented
+        return self.id == other.id
+
+    def __hash__(self) -> int:
+        return hash(self.id)

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +198 to +206
* Edge
* [Edge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge.py)
* [Edge Directed](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_directed.py)
* [Edge Hyper](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_hyper.py)
* [Edge Self](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_self.py)
* [Edge Type](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_type.py)
* [Edge Undirected](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_undirected.py)
* [Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/graph.py)
* [Test Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/test_vertex.py)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix list indentation to satisfy MD007 and match surrounding style.

Current bullets under “Graphs > Edge” are over‑indented. Apply:

-    * Edge
-      * [Edge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge.py)
-      * [Edge Directed](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_directed.py)
-      * [Edge Hyper](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_hyper.py)
-      * [Edge Self](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_self.py)
-      * [Edge Type](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_type.py)
-      * [Edge Undirected](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_undirected.py)
-    * [Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/graph.py)
-    * [Test Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/test_vertex.py)
+  * Edge
+    * [Edge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge.py)
+    * [Edge Directed](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_directed.py)
+    * [Edge Hyper](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_hyper.py)
+    * [Edge Self](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_self.py)
+    * [Edge Type](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_type.py)
+    * [Edge Undirected](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_undirected.py)
+  * [Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/graph.py)
+  * [Test Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/test_vertex.py)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* Edge
* [Edge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge.py)
* [Edge Directed](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_directed.py)
* [Edge Hyper](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_hyper.py)
* [Edge Self](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_self.py)
* [Edge Type](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_type.py)
* [Edge Undirected](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_undirected.py)
* [Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/graph.py)
* [Test Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/test_vertex.py)
* Edge
* [Edge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge.py)
* [Edge Directed](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_directed.py)
* [Edge Hyper](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_hyper.py)
* [Edge Self](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_self.py)
* [Edge Type](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_type.py)
* [Edge Undirected](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_undirected.py)
* [Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/graph.py)
* [Test Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/test_vertex.py)
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

198-198: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


199-199: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


200-200: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


201-201: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


202-202: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


203-203: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


204-204: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


205-205: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


206-206: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🤖 Prompt for AI Agents
In DIRECTORY.md around lines 198 to 206, the six bullet links under the "Edge"
subheading are over‑indented and break MD007; unindent those lines so the list
items for Edge are at the same indentation level as the following top-level list
entries (e.g., Graph and Test Vertex). Ensure the bullets use the same number of
leading spaces/tabs as the surrounding lists (consistent indentation style) so
the nested list structure and MD007 lint rule are satisfied.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
algorithms/arrays/two_sum/__init__.py (1)

33-61: Apply the same type safety fix to two_sum_with_pointers.

This function has the same implicit None return issue that was just fixed in two_sum. It should also explicitly return an empty list when no pair is found.

Apply this diff:

         else:
             last_pointer -= 1
+    return []
puzzles/arrays/maxlen_contiguous_binary_subarray/test_maxlen_contiguous_binary_subarray.py (1)

53-104: Dead code with critical errors.

The tests() function is defined but never invoked, making it unreachable. Additionally, it contains several critical issues:

  1. Line 80: shuffle() is not imported. Should be random.shuffle().
  2. Lines 82, 92: test.assert_equals() references an undefined test object.
  3. Lines 82, 92: binarray() function is not defined or imported.

If this function is intended for use, apply these fixes and ensure it's called:

    def shuffs(s):
-       shuffle(s)
+       random.shuffle(s)
        answer = check(s)
-       test.assert_equals(binarray(s.copy()), answer)
+       self.assertEqual(find_max_length(s.copy()), answer)
                answer = check(s)
-               test.assert_equals(binarray(s.copy()), answer)
+               self.assertEqual(find_max_length(s.copy()), answer)
                if flag:

If this code is no longer needed, remove the entire tests() function and the random import.

Do you want me to help refactor this into proper unittest test methods, or would you prefer to remove it entirely?

algorithms/two_pointers/sort_colors/__init__.py (1)

40-72: Sync sort_colors_v2 docstring with implementation and clarify its role vs sort_colors.

The implementation looks correct (standard Dutch national flag), but:

  • The docstring describes a “two pointers: low and high” algorithm that iterates with low, which doesn’t match the actual code using red, blue, and index i.
  • sort_colors_v2 is effectively the same algorithm and complexity as sort_colors, just with different variable names, so the distinction between the two functions isn’t obvious from the docs.

I’d suggest:

  1. Update the docstring so it reflects the actual implementation and makes clear how v2 differs conceptually from sort_colors (or explicitly state it’s just an alternative formulation).

  2. Optionally add unit tests for sort_colors_v2 alongside the existing tests to ensure both implementations stay in sync.

Example docstring tweak:

 def sort_colors_v2(colors: List[int]) -> List[int]:
-    """
-    Sorts a list of 0s, 1s, and 2s in place using two pointers
-    ...
-    The algorithm works by iterating through the list with the low pointer. If it encounters a 0, it simply increments
-    low. If it encounters a 2, it decrements high. If it encounters a 1, it swaps it with the element at the high index
-    and decrements high. This swapping process ensures that all the 1s are placed between the 0s and the 2s.
-    """
+    """
+    Sorts a list of 0s, 1s, and 2s in place using boundary pointers.
+
+    This variant also uses the Dutch national flag idea: `red` tracks the next
+    position for 0, `blue` tracks the next position for 2, and `i` scans the
+    list, swapping elements into the correct region.
+    """

If you’d like, I can also sketch matching tests for sort_colors_v2 based on the existing sort_colors test cases.

algorithms/arrays/subsequence/__init__.py (1)

40-72: Align v2 edge-case behavior and docstring with existing API contract

is_valid_subsequence_v2’s core two-pointer logic is correct for typical non-empty inputs, but there are a couple of contract/consistency nits:

  • The docstring states array and sequence are non-empty, and is_valid_subsequence enforces this (returns False when either is empty). In contrast, v2 currently treats an empty sequence as a valid subsequence (returns True when sequence == [], even if array is empty). That means callers will see different results between v1 and v2 on the same inputs outside the “non-empty” precondition.
  • The time/space complexity comments still describe a “iterate array then sequence” style algorithm; v2 actually does a single pass with two indices.

If you want v1 and v2 to be interchangeable, consider either (a) adding the same empty-input guard as v1, or (b) explicitly relaxing the precondition in both functions’ docstrings and updating tests to cover the empty-sequence case. Also, you can slightly tighten the tail by returning the comparison directly:

-    if i == len(sequence):
-        return True
-    return False
+    return i == len(sequence)
datastructures/trees/binary/search_tree/__init__.py (1)

445-462: Fix incorrect in-order traversal implementation.

The method has multiple issues:

  1. The node parameter is never used; the method always operates on self.root
  2. Recursive calls on lines 456 and 461 discard their return values
  3. The result only contains the root's data, not a full traversal

Apply this fix to implement correct recursive in-order traversal:

 def in_order_recurse(self, node: BinaryTreeNode) -> List[T]:
     """
     Another type of Depth First Search (DFS) that traverses the tree from the left to middle to right of the tree.
     This type of search will begin at the left node and check if that node has a left child and continually check
     until that left node is a leaf(has no children) and will then print its data and "bubble up" back to the
     current node and execute that (in this case print it) and then print the right node. The same procedure is
     executed for the right side of the tree.
     """
     result = []
-    if self.root:
-        if self.root.left:
-            self.in_order_recurse(self.root.left)
+    if node:
+        if node.left:
+            result.extend(self.in_order_recurse(node.left))

-        result.append(self.root.data)
+        result.append(node.data)

-        if self.root.right:
-            self.in_order_recurse(self.root.right)
+        if node.right:
+            result.extend(self.in_order_recurse(node.right))
     return result
datastructures/trees/heaps/binary/min_heap/__init__.py (1)

62-81: Fix the index mapping logic in __bubble_up and __bubble_down — current implementation causes KeyError.

The double-indexing in lines 73–79 is incorrect. After swapping heap elements, attempting to use self.idx_of_element[self.heap[idx]] as a key fails because self.idx_of_element maps HeapNode objects to integers, not integers to values. This causes KeyError when connect_sticks_2 calls insert_data repeatedly.

Correct the index updates to directly map the swapped nodes to their new positions:

parent_node = self.heap[parent_idx]
child_node = self.heap[idx]
self.idx_of_element[parent_node] = idx
self.idx_of_element[child_node] = parent_idx

Apply the same fix to __bubble_down (lines 52–58).

datastructures/trees/binary/node.py (1)

163-176: Return type annotation inconsistent with implementation.

The return type List["BinaryTreeNode"] | None suggests None can be returned, but all branches return lists (including empty [] on line 176). Either remove | None from the type hint or update to always return a list.

     @property
-    def children(self) -> List["BinaryTreeNode"] | None:
+    def children(self) -> List["BinaryTreeNode"]:
         """Returns children of this node.
         Returns:
             List: children of this node in a list
         """
🟡 Minor comments (12)
datastructures/trees/trie/suffix/suffix_tree_node.py-14-21 (1)

14-21: Remove unused best_index attribute.

The best_index attribute (line 17) is initialized but never used. The search_best_index() method exclusively uses best_info to track and return the best word match, making best_index dead code.

datastructures/trees/binary/search_tree/__init__.py-252-301 (1)

252-301: Add input validation for k.

The method lacks validation for the input parameter k. Consider adding checks for edge cases such as k <= 0 or k exceeding the number of nodes in the tree. While the current implementation returns None for invalid inputs, this behavior should be documented or made explicit through validation.

Consider applying this enhancement:

 def find_kth_largest(self, k: int) -> Optional[BinaryTreeNode]:
     """
     Finds the kth largest value in a binary search tree. This uses a reverse in order traversal moving from right
     to root to left until the kth largest node can be found. We don't have to traverse the whole tree since binary
     search trees are already ordered following the property of the right subtree has nodes which have the left
     sub-tree always less than their parent and the right subtree has nodes with values that are either equal to or
     greater than the parent. With this property in mind, we perform a reverse in order traversal to be able to move
     from right to root to left to find the kth largest node in the tree.

     Complexity:
     Time: O(h + k): where h is the height of the tree and k is the input
     Space: O(h): where h is the height of the tree.

     Args:
         k (int): The kth largest value to find
     Returns:
         Optional[BinaryTreeNode]: The kth largest node
     """
+    if k <= 0:
+        return None

     # This is a helper class that helps to track the algorithm's progress of traversing the tree
algorithms/arrays/two_sum_less_k/__init__.py-10-10 (1)

10-10: Misleading docstring: input does not need to be pre-sorted.

The docstring states nums (List[int]): A sorted list of integers, but the function sorts the input at line 18. Either update the docstring to clarify that the input does not need to be sorted, or remove the sorting step if the precondition is that the input must be sorted.

Apply this diff to fix the docstring:

-        nums (List[int]): A sorted list of integers
+        nums (List[int]): A list of integers
algorithms/search/binary_search/maxruntime_n_computers/README.md-41-48 (1)

41-48: Fix README pseudo-code and complexity typos

The algorithm description is accurate, but there are a few typos that could confuse readers:

  • Line 43: rught should be right.
  • Lines 62–63: extra closing parentheses in O(n⋅logT)) and T = sum(batteries) // n).
  • Line 68: "os""is".

You could update these like:

-   - Calculate `mid = right - (rught - left) // 2` (biases the midpoint toward the higher end to avoid infinite loops).
+   - Calculate `mid = right - (right - left) // 2` (biases the midpoint toward the higher end to avoid infinite loops).

- The time complexity of the solution is `O(n⋅logT))`, where `n` is the number of batteries and `T` is the total power of 
- one computer, `T = sum(batteries) // n)`.
+ The time complexity of the solution is `O(n ⋅ log T)`, where `n` is the number of batteries and `T` is the total power of 
+ one computer, `T = sum(batteries) // n`.

- The space complexity of the solution os `O(1)` because no extra space is used
+ The space complexity of the solution is `O(1)` because no extra space is used

As indicated by the static analysis hints.

Also applies to: 60-68

puzzles/permutations_check/test_check.py-31-45 (1)

31-45: Fixtures reformat is good, but one game length and some docstrings look inconsistent

The multiline games literals in test_4/test_5 are clearer and easier to maintain, but there are a couple of details worth checking:

  • In test_4, the first game [1, 6, 4, 5, 2] has only 5 entries while n = 6. If each game is meant to be a full permutation of 1..n, this likely wants a sixth value (perhaps 3?) to avoid a silent mismatch between n and the fixture data.
  • The docstrings for test_3 and test_4 say “should return False”, but the corresponding assertions use assertTrue. Either the expected outcome or the docstring is stale; consider aligning them to avoid confusion for future readers.

Also applies to: 47-61

puzzles/heap/min_cost_to_connect_sticks/__init__.py-45-86 (1)

45-86: Correct the docstring: wrong example and space complexity.

Two issues in the docstring:

  1. The example is incorrect (same issue as in connect_sticks). The minimum cost for [3, 4, 5] is 19, not 12.

  2. The space complexity claim of O(1) is incorrect for this implementation. Lines 62-65 create a new list of HeapNode objects, requiring O(n) space.

Apply this diff to correct the docstring:

-    For example, given the list [3, 4, 5], the minimum cost to connect all sticks is 12 (3 + 4 + 5).
+    For example, given the list [3, 4, 5], the minimum cost to connect all sticks is 19.
 
     The time complexity of this function is O(nlogn), where n is the number of sticks.
-    The space complexity of this function is O(1), because the heap is built in place from the input list
+    The space complexity of this function is O(n), because it creates HeapNode objects for each stick.

The implementation logic is correct, though it duplicates the algorithm from connect_sticks.

Consider whether maintaining two implementations is necessary, or if one sufficiently demonstrates the algorithm.

puzzles/heap/min_cost_to_connect_sticks/__init__.py-6-42 (1)

6-42: Correct the example in the docstring.

The docstring example is incorrect. For [3, 4, 5], the minimum cost is 19, not 12:

  • First merge: 3 + 4 = 7 (cost: 7)
  • Second merge: 7 + 5 = 12 (cost: 12)
  • Total cost: 7 + 12 = 19

Apply this diff to correct the example:

-    For example, given the list [3, 4, 5], the minimum cost to connect all sticks is 12 (3 + 4 + 5).
+    For example, given the list [3, 4, 5], the minimum cost to connect all sticks is 19.

The implementation logic is correct and efficiently uses in-place heapification.

DIRECTORY.md-251-251 (1)

251-251: Typo: "Revese" should be "Reverse".

The filename reference contains a typo.

-      * [Test Singly Linked Revese](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_revese.py)
+      * [Test Singly Linked Reverse](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_reverse.py)

Note: This may require renaming the actual file as well.

datastructures/streams/stream_checker/README.md-6-7 (1)

6-7: Incorrect example: "cat" is not in the word list.

The example states words = ["dog"] but then claims the suffix "cat" matches the word "cat" from the list. Since "cat" is not in ["dog"], this example is incorrect. Consider updating to words = ["dog", "cat"] or choosing a matching example like detecting "dog" in stream "adog".

-For example, if words = ["dog"] and the stream adds the characters 'd', 'c', 'a' , and 't' in sequence, the algorithm 
-should detect that the suffix "cat" of the stream "dcat" matches the word "cat" from the list.
+For example, if words = ["cat", "dog"] and the stream adds the characters 'd', 'c', 'a', and 't' in sequence, the algorithm 
+should detect that the suffix "cat" of the stream "dcat" matches the word "cat" from the list.
datastructures/trees/binary/tree/binary_tree.py-420-432 (1)

420-432: Unreachable code: duplicate condition check.

Lines 427-428 have the same condition as lines 424-425. Since line 425 returns, lines 427-428 are never executed. This appears to be a copy-paste error—perhaps the intent was to check for nodes with exactly one child?

         def is_perfect_helper(root: BinaryTreeNode, level: int = 0) -> bool:
             if root is None:
                 return True

             if root.left is None and root.right is None:
                 return depth == level + 1

-            if root.left is None and root.right is None:
+            if root.left is None or root.right is None:
                 return False

             return is_perfect_helper(root.left, level + 1) and is_perfect_helper(
                 root.right, level + 1
             )
datastructures/linked_lists/singly_linked_list/test_singly_linked_revese.py-1-6 (1)

1-6: Fix filename typo: "revese" → "reverse".

The filename test_singly_linked_revese.py contains a typo. Rename to test_singly_linked_reverse.py for consistency and discoverability.

algorithms/sliding_window/repeated_dna_sequences/README.md-84-88 (1)

84-88: Fix small documentation typos and align snippet type hints with their return values.

A few minor doc issues are worth adjusting:

  • Around line 86, the mapping text ends with 3 → 'T (missing the closing quote). Updating to 3 → 'T' will match the actual mapping dictionary.
  • In the step‑by‑step snippets (find_repeated_dna_sequences in the “Step 1” and “Step 2” sections), the function is annotated as returning List[str] but returns an encoded list of ints or a single hash value. To avoid confusion, either:
    • Change the return type hints to match what those pedagogical snippets actually return, or
    • Rename them to helper names (e.g., encode_dna_sequence, compute_initial_hash) and keep accurate annotations there.
  • At line 240, “10-letter long sequences” should be “10-letter-long sequences” (or simply “10-letter sequences”) to fix the hyphenation noted by the linter.

These tweaks will make the README consistent with the implementation and type hints.

Also applies to: 90-103, 143-165, 240-240

🧹 Nitpick comments (48)
puzzles/poker/__init__.py (1)

27-59: Consider simplifying the deeply nested ternary for readability.

The reformatting makes the structure visible, but an 8-level nested ternary is still hard to follow. A lookup-based approach would be clearer:

def hand_rank(hand):
    card_ranks = ["..23456789TJQKA".index(r) for r, s in hand]
    groups = [(card_ranks.count(i), i) for i in set(card_ranks)]
    groups.sort(reverse=True)
    counts, ranks = zip(*groups)
    if ranks == [14, 5, 4, 3, 2]:
        ranks = [5, 4, 3, 2, 1]
    straight = (len(counts) == 5) and (max(ranks) - min(ranks) == 4)
    flush = len(set([s for r, s in hand])) == 1

    count_ranks = {
        (5,): 9,
        (4, 1): 7,
        (3, 2): 6,
        (3, 1, 1): 3,
        (2, 2, 1): 2,
        (2, 1, 1, 1): 1,
    }

    if straight and flush:
        score = 8
    elif counts in count_ranks:
        score = count_ranks[counts]
    elif flush:
        score = 5
    elif straight:
        score = 4
    else:
        score = 0

    return score, ranks

This separates the pattern-matching logic from the special cases (straight/flush combinations).

datastructures/sets/union_find/__init__.py (1)

1-45: Solid DSU implementation with union-by-rank and path compression.

The implementation is correct. One minor consideration: find and union don't validate that indices are within bounds. An out-of-range index will raise IndexError, which is acceptable but could be confusing. Consider adding explicit bounds checking with a descriptive error message if this is intended for educational use.

 def find(self, i: int) -> int:
     """Finds the representative (root) of the set containing element 'i'."""
+    if not 0 <= i < len(self.root):
+        raise IndexError(f"Element {i} is out of bounds for size {len(self.root)}")
     if self.root[i] == i:
         return i
algorithms/arrays/two_sum/__init__.py (1)

30-30: LGTM! Consider documenting the empty list return case.

The explicit return [] improves type safety by ensuring the function always returns List[int] as declared. However, the docstring (lines 19-20) doesn't mention that an empty list is returned when no pair sums to the target.

Consider updating the docstring:

     Returns:
-        List of two numbers that sum up to the target
+        List of indices of two numbers that sum up to the target, or empty list if no such pair exists
datascience/numeric_python/statistics/TestChecker.py (2)

1-1: Use proper docstring format.

The module-level documentation should use triple-quoted strings to be recognized as a docstring.

-"Class to test truths of functions"
+"""Class to test truths of functions"""

5-6: Unused instance variable self.n.

The __init__ method stores n but it's never used. Consider removing it or documenting its purpose if it's intended for future use.

algorithms/graphs/frog_position_after_t_seconds/README.md (1)

61-61: Use "cannot" instead of "can not".

Per standard style conventions, "cannot" is the preferred form unless emphasizing "not".

Apply this diff:

-      If time_left == 0, return p because the frog arrived earlier and can not move, so it remains on the target.
+      If time_left == 0, return p because the frog arrived earlier and cannot move, so it remains on the target.
algorithms/graphs/frog_position_after_t_seconds/__init__.py (2)

18-22: Remove unused parent parameter from queue tuples.

The parent variable is stored in the queue state but never used. The visited set already prevents revisiting nodes, making parent tracking redundant.

Apply this diff:

-    queue = deque([(1, -1, 1.0, 0)])
+    queue = deque([(1, 1.0, 0)])
     visited = {1}
 
     while queue:
-        vertex, parent, prob, time = queue.popleft()
+        vertex, prob, time = queue.popleft()

And update line 53:

             for neighbor in unvisited_neighbors:
                 visited.add(neighbor)
-                queue.append((neighbor, vertex, next_prob, time + 1))
+                queue.append((neighbor, next_prob, time + 1))

76-91: Avoid iterating over neighbors twice.

The code iterates over graph[u] twice: once to count unvisited neighbors (lines 77-79), then again to enqueue them (lines 88-91). Collect unvisited neighbors in a single pass for better efficiency.

Apply this diff:

-            cnt_unvisited = 0
+            unvisited = []
             for v in graph[u]:
                 if not visited[v]:
-                    cnt_unvisited += 1
+                    unvisited.append(v)
 
             if u == target:
-                if time_left == 0 or cnt_unvisited == 0:
+                if time_left == 0 or len(unvisited) == 0:
                     return p
                 return 0.0
 
-            if cnt_unvisited > 0:
-                split = p / cnt_unvisited
-                for v in graph[u]:
-                    if not visited[v]:
-                        visited[v] = True
-                        q.append((v, split))
+            if unvisited:
+                split = p / len(unvisited)
+                for v in unvisited:
+                    visited[v] = True
+                    q.append((v, split))
datastructures/trees/trie/__init__.py (1)

3-4: Consider importing from the suffix subpackage instead of individual modules.

The suffix/__init__.py already exports SuffixTreeNode and SuffixTree. Importing from the subpackage directly would be more consistent and reduce coupling to internal module structure.

 from datastructures.trees.trie.trie_node import TrieNode
 from datastructures.trees.trie.trie import Trie
-from datastructures.trees.trie.suffix.suffix_tree_node import SuffixTreeNode
-from datastructures.trees.trie.suffix.suffix_tree import SuffixTree
+from datastructures.trees.trie.suffix import SuffixTreeNode, SuffixTree
datastructures/trees/trie/trie_node.py (3)

6-8: Remove commented-out code.

The commented line # self.char = char appears to be dead code from an earlier implementation. Remove it to improve readability.

     def __init__(self):
-        # self.char = char
         """
         Initializes a TrieNode instance.

26-41: Add return type hint for consistency.

The insert method is missing a return type hint, while search_prefix has one. Add -> None for consistency with the PR checklist requirement that all function parameters and return values are annotated with type hints.

-    def insert(self, word: str, index: int):
+    def insert(self, word: str, index: int) -> None:

38-41: Type inconsistency with float("inf") comparison.

The expression curr.index or float("inf") mixes Optional[int] with float, which works at runtime but creates a subtle type inconsistency. The Trie.insert method in trie.py uses the same pattern, so this is consistent with the codebase, but consider using sys.maxsize (as done in types.py) for type consistency.

+import sys
+
 # In the insert method:
-            curr.index = min(curr.index or float("inf"), index)
+            curr.index = min(curr.index if curr.index is not None else sys.maxsize, index)
datastructures/trees/trie/suffix/suffix_tree.py (1)

14-16: Unnecessary super().__init__() call.

SuffixTree doesn't inherit from any explicit parent class, so calling super().__init__() is redundant. This may be a leftover from refactoring.

Apply this diff to remove the unnecessary call:

     def __init__(self):
-        super().__init__()
         self.root = SuffixTreeNode()
datastructures/arrays/matrix/settozero/__init__.py (1)

4-55: Algorithm looks correct; consider tightening the docstring/type info.

The in-place marker-based implementation is clean and correct for rectangular matrices (including zeros in the first row/column and empty-matrix cases). Nothing blocking here.

Two small polish points you may want to address:

  • Clarify the behavior and mutability in the docstring (it currently sounds a bit global and doesn't explicitly say "in place").
  • Align the docstring type with the annotation.

Example docstring tweak:

-def set_matrix_zeros(mat: List[List[int]]) -> List[List[int]]:
-    """
-    Sets all elements in a matrix to zero if any row or column has at least one zero element.
+def set_matrix_zeros(mat: List[List[int]]) -> List[List[int]]:
+    """
+    For every row or column that contains at least one zero, sets all elements
+    in that row or column to zero, modifying the matrix in place.
@@
-    Args:
-        mat (List): input matrix
+    Args:
+        mat (List[List[int]]): input matrix
datastructures/arrays/matrix/settozero/test_set_matrix_zero.py (1)

1-50: Good coverage; add edge cases and consider import/run-mode consistency.

Tests exercise several meaningful patterns and look correct. A few non-blocking improvements:

  1. Edge cases that would strengthen regression coverage

    • A matrix with a zero only in the first column (and none in the first row) to fully exercise first_col_has_zero.
    • An empty matrix ([]) and a matrix with an empty first row ([[]]) to lock in the early-return behavior.
    • Optionally, 1×N / N×1 matrices.

    For example:

 class SetMatrixZeroTestCase(unittest.TestCase):
@@
     def test_5(self):
         matrix = [[3, 5, 2, 0], [1, 0, 4, 6], [7, 3, 2, 4]]
         expected = [[0, 0, 0, 0], [0, 0, 0, 0], [7, 0, 2, 0]]
         actual = set_matrix_zeros(matrix)
         self.assertEqual(expected, actual)
+
+    def test_zero_in_first_column_only(self):
+        matrix = [[1, 2, 3], [0, 5, 6], [7, 8, 9]]
+        expected = [[0, 2, 3], [0, 0, 0], [0, 8, 9]]
+        actual = set_matrix_zeros(matrix)
+        self.assertEqual(expected, actual)
+
+    def test_empty_and_empty_row(self):
+        self.assertEqual([], set_matrix_zeros([]))
+        self.assertEqual([[]], set_matrix_zeros([[]]))
  1. Asserting in-place semantics (if that’s part of the contract)
    If you want to guarantee that set_matrix_zeros mutates the input rather than returning a new matrix, you could add:
matrix = [[1, 0], [3, 4]]
actual = set_matrix_zeros(matrix)
self.assertIs(matrix, actual)
  1. Relative import vs direct execution

from . import set_matrix_zeros assumes the test is run as part of the package (e.g., python -m unittest ...). Running this file directly (python test_set_matrix_zero.py) will typically fail with an attempted relative import error.

If you expect direct execution to work, consider switching to an absolute import appropriate for your project layout or dropping the __main__ block and relying solely on your test runner.

To confirm your desired behavior, run tests the way your CI does (e.g., python -m unittest datastructures.arrays.matrix.settozero.test_set_matrix_zero) and decide whether direct python path/to/test_set_matrix_zero.py should also be supported.

datastructures/arrays/subarrays_with_fixed_bounds/README.md (1)

3-16: Clarify bounds relationship and align naming with implementation

The README is clear, but you might want to (a) mention explicitly that minK ≤ maxK (the implementation treats min_k > max_k as yielding 0), and (b) optionally align naming (minK/maxK vs min_k/max_k) with the code for easier cross‑referencing.

datastructures/arrays/subarrays_with_fixed_bounds/__init__.py (1)

5-28: Minor docstring polish for clarity

A couple of small cleanups you could consider:

  • Grammar: “subarrays … which satisfy the following conditions” instead of “which satisfies”.
  • Possibly add a short note that any element < min_k or > max_k makes a subarray invalid for counting, matching the implementation logic.
  • Optionally align parameter names (min_k, max_k) in the text with the function signature.

Purely cosmetic, feel free to skip if you’re satisfied with the current wording.

datastructures/arrays/subarrays_with_fixed_bounds/test_subarrays_with_fixed_bounds.py (1)

5-56: Good coverage; consider a couple of extra edge‑case tests

The tests nicely exercise the main scenarios, including the LeetCode‑style example with an out‑of‑range element and the min_k == max_k cases. As a small enhancement, you could add tests for:

  • nums = [] (should return 0 per implementation)
  • min_k > max_k (also returns 0)

That would lock in the guard branch at Lines 29–31 of the implementation.

algorithms/arrays/two_sum_less_k/__init__.py (1)

18-18: In-place sorting mutates the input array.

The nums.sort() call modifies the caller's array, which is a side effect that may be unexpected. Consider using nums = sorted(nums) to avoid mutating the input, or clearly document this behavior in the docstring.

Apply this diff to avoid mutation:

-    # sort the numbers in ascending order to facilitate binary search
-    nums.sort()
+    # sort the numbers in ascending order to facilitate binary search
+    nums = sorted(nums)

Alternatively, document the side effect:

     """
     Finds the maximum sum of two elements in a given list of numbers that is less than k.
     Uses binary search to achieve a time complexity of O(n log n) and find the maximum sum of two elements
     that is less than k. It takes the nums array and the target value k as input.
+    
+    Note: This function sorts the input list in-place.
+    
     Args:
-        nums (List[int]): A sorted list of integers
+        nums (List[int]): A list of integers (will be sorted in-place)
         k int: The target value to search for
     Returns:
         The maximum sum of two elements that is less than k
     """
algorithms/arrays/two_sum_less_k/test_two_sum.py (1)

1-48: Good test coverage for common scenarios.

The test suite covers multiple important cases including valid pairs, no valid pairs, repeated elements, and the boundary case where the sum equals k. The test expectations are all correct based on manual verification of the algorithm.

Consider adding edge case tests for improved robustness:

def test_empty_array(self):
    """numbers = [], k = 10"""
    numbers = []
    k = 10
    expected = -1
    actual = two_sum_less_than_k(numbers, k)
    self.assertEqual(expected, actual)

def test_single_element(self):
    """numbers = [5], k = 10"""
    numbers = [5]
    k = 10
    expected = -1
    actual = two_sum_less_than_k(numbers, k)
    self.assertEqual(expected, actual)

def test_negative_numbers(self):
    """numbers = [-3, -1, 2, 4], k = 1"""
    numbers = [-3, -1, 2, 4]
    k = 1
    expected = -2  # -3 + 1 = -2
    actual = two_sum_less_than_k(numbers, k)
    self.assertEqual(expected, actual)
algorithms/search/binary_search/maxruntime_n_computers/__init__.py (1)

4-72: Binary search implementation looks correct; consider optional early-exit in feasibility check

The overall approach in max_runtime, can_run_for, and max_run_time_2 is sound and matches the documented binary-search strategy (bounds, monotonicity, and feasibility condition all look correct).

If you expect very large batteries lists, you could micro-optimize can_run_for by short‑circuiting once enough power is accumulated, e.g.:

def can_run_for(batteries: List[int], n: int, target_time: int) -> bool:
    power_needed = n * target_time
    power_available = 0
    for battery in batteries:
        power_available += min(battery, target_time)
        if power_available >= power_needed:
            return True
    return False

This avoids traversing the full list when the condition is met early. Totally optional; the current version is already clear and correct.

algorithms/fast_and_slow/happy_number/test_happy_number.py (1)

116-121: Good additional negative-case coverage for the fast/slow implementation

test_10 correctly asserts that is_happy_number_2(20) is False (20 falls into a non‑1 cycle), which adds a useful extra unhappy-path check for the new implementation. No issues from a correctness standpoint.

If you want to reduce duplication between the two test classes, you could consider parameterizing the cases and running them against both is_happy_number and is_happy_number_2, but that’s optional.

algorithms/fast_and_slow/happy_number/__init__.py (1)

38-102: Fast/slow happy-number implementation looks correct; consider minor reuse & input-contract tweaks

The is_happy_number_2 implementation correctly applies Floyd’s cycle detection: the digit-sum helper is correct for non-negative integers, the fast/slow update loop is standard, and the final check on fast_pointer == 1 yields the right result for happy vs. unhappy numbers. No correctness issues spotted for the documented “positive integer” domain.

Two optional improvements you might consider:

  • Factor out sum_of_squared_digits / get_next into a shared top-level helper so both is_happy_number variants reuse the same logic instead of duplicating the digit-sum loop.
  • Either:
    • enforce the documented precondition (e.g., raise ValueError for n <= 0), or
    • explicitly document that non-positive inputs are treated as “not happy” and return False without raising, to make the behavior clear.
puzzles/heap/min_cost_to_connect_sticks/test_min_cost_connect_sticks.py (1)

1-118: Well-structured test suite with good coverage.

The test file properly validates both implementations with identical test cases, ensuring consistency. The edge case for a single stick (returning 0) is covered.

Consider adding a test case for an empty list to ensure both implementations handle this edge case gracefully:

def test_empty_list(self):
    sticks = []
    expected = 0
    actual = connect_sticks(sticks)
    self.assertEqual(expected, actual)
DIRECTORY.md (1)

26-27: Inconsistent list indentation throughout the file.

The added entries use 4-space and 6-space indentation, but MD007 expects 2-space increments. This affects multiple sections beyond the Edge entries already flagged. Consider applying consistent 2-space indentation across all changed sections for MD007 compliance.

Also applies to: 80-84

datastructures/trees/binary/tree/README.md (1)

377-393: Consider adding solution approach for consistency.

The other problem sections in this README include detailed solution approaches with algorithm descriptions and complexity analysis. This section only describes the problem. Consider adding the solution approach (greedy DFS from leaves) for consistency.

Also, the constraint Node.data == 0 on line 389 appears to be copied from a LeetCode problem context where the node values are irrelevant (all zeros). If this is intentional, a brief clarification would help readers understand it's a placeholder value, not a meaningful constraint.

puzzles/heap/maximal_score_after_k_operations/README.md (1)

3-6: Redundant problem statement introduction.

Lines 3-6 contain two overlapping introductions: "You are given an array of integers nums..." and "You are given a 0-indexed array of integer nums...". Consider consolidating into a single, clear introduction.

-You are given an array of integers nums and an integer k. You want to perform the following operation exactly k times:
-
-You are given a 0-indexed array of integer nums and an integer k. Your task is to maximize a score through a series of 
-operations. Initially, your score is set to 0
+You are given a 0-indexed integer array `nums` and an integer `k`. Your task is to maximize a score through a series of 
+operations. Initially, your score is set to 0.
datastructures/trees/binary/tree/test_binary_tree_min_camera_cover.py (1)

1-60: LGTM! Test cases provide good coverage for min_camera_cover.

The tests cover important scenarios: single node (root must have camera), small balanced and imbalanced trees, and deeper structures. The expected values align with the greedy algorithm's behavior.

Consider adding descriptive test method names (e.g., test_single_node_requires_one_camera) for better readability.

datastructures/streams/stream_checker/__init__.py (1)

26-39: Unnecessary reversal of words list in __build_trie.

Line 28 iterates self.words[::-1], which reverses the order of words. However, only reversing each word's characters (line 35) is needed for the reversed trie approach. The order in which words are inserted doesn't affect correctness, but reversing the words list is confusing and adds unnecessary overhead.

     def __build_trie(self):
         # insert the words in reverse order into the trie
-        for word in self.words[::-1]:
+        for word in self.words:
             # 1. track max length for deque optimization
             if len(word) > self.max_len:
                 self.max_len = len(word)
datastructures/trees/binary/tree/binary_tree.py (3)

626-645: Instance attribute pollution for tracking path length.

self.path_length is set as an instance attribute (line 630) to track state during recursion. This leaves stale state on the instance after the method completes. Consider using nonlocal with a local variable or a mutable container like the longest_zig_zag_stack approach.

     def longest_zig_zag(self) -> int:
         if self.root is None or (self.root.left is None and self.root.right is None):
             return 0

-        self.path_length = 0
+        path_length = [0]

         def dfs(node: Optional[BinaryTreeNode], go_left: bool, steps: int):
             if node is not None:
-                self.path_length = max(self.path_length, steps)
+                path_length[0] = max(path_length[0], steps)
                 if go_left:
                     dfs(node.left, False, steps + 1)
                     dfs(node.right, True, 1)
                 else:
                     dfs(node.left, False, 1)
                     dfs(node.right, True, steps + 1)

         dfs(self.root, False, 0)
         dfs(self.root, True, 0)

-        return self.path_length
+        return path_length[0]

843-856: deserialize returns BinaryTreeNode, not BinaryTree.

The method returns a BinaryTreeNode rather than a BinaryTree instance, which is inconsistent with serialize() being an instance method. Consider returning BinaryTree(dfs(...)) or document this asymmetry.

     @staticmethod
-    def deserialize(tree_str: str) -> Optional[BinaryTreeNode]:
+    def deserialize(tree_str: str) -> "BinaryTree":
         def dfs(nodes: Iterator[str]) -> Optional[BinaryTreeNode]:
             data = next(nodes)

             if data == "x":
-                return
+                return None

             current = BinaryTreeNode(data)
             current.left = dfs(nodes)
             current.right = dfs(nodes)
             return current

-        return dfs(iter(tree_str.split()))
+        return BinaryTree(dfs(iter(tree_str.split())))

16-17: Stub methods should raise NotImplementedError.

Methods next, has_next, insert_node, paths, and increasing_order_traversal are stubs that return None implicitly. Consider raising NotImplementedError to make incomplete status explicit and prevent silent failures.

     def next(self) -> int:
-        pass
+        raise NotImplementedError("next() is not implemented")

     def has_next(self) -> bool:
-        pass
+        raise NotImplementedError("has_next() is not implemented")

     def increasing_order_traversal(self) -> TreeNode:
-        pass
+        raise NotImplementedError("increasing_order_traversal() is not implemented")

     def insert_node(self, value) -> TreeNode:
-        pass
+        raise NotImplementedError("insert_node() is not implemented")

     def paths(self) -> list:
-        pass
+        raise NotImplementedError("paths() is not implemented")

Also applies to: 64-68, 78-82

datastructures/trees/node.py (2)

15-33: Clarify parent semantics and tighten its type hint

  • Accepting and storing parent directly is fine, but this changes the invariants of the node: callers now need to keep parent and any children container (if present elsewhere) in sync manually. If the class (or subclasses) expose a children list, consider documenting that responsibility clearly or updating the parent’s children when a parent is passed in, to avoid subtle inconsistencies.
  • For better typing of the generic, you may want to narrow the annotation to the parameterized type:
-    def __init__(
-        self, value: T, key: Optional[Any] = None, parent: Optional["TreeNode"] = None
-    ):
+    def __init__(
+        self,
+        value: T,
+        key: Optional[Any] = None,
+        parent: Optional["TreeNode[T]"] = None,
+    ):

37-56: Review __eq__ / __hash__ behavior and align with typing & Python data model

This block introduces several behavioral and typing changes worth double‑checking:

  1. Behavioral change from identity to value‑based equality

    Previously, TreeNode used object identity for ==. Now two different nodes with the same .data compare equal, regardless of key or parent. That can be exactly what you want, but it may surprise existing tree algorithms that distinguish nodes by position or key. Please confirm this is intentional across current usages.

  2. Prefer other: object and NotImplemented for unsupported types

    The canonical __eq__ signature and behavior are:

  • def eq(self, other: "TreeNode[T]") -> bool:
  •    ...
    
  •    if not isinstance(other, TreeNode):
    
  •        return False
    
  •    ...
    
  •    if other.data == self.data:
    
  •        return True
    
  •    return False
    
  • def eq(self, other: object) -> bool:
  •    """Checks if this node is equal to another node based on the data it contains."""
    
  •    if not isinstance(other, TreeNode):
    
  •        return NotImplemented
    
  •    return self.data == other.data
    
    
    This plays better with type checkers and with Python’s symmetric equality handling.
    
    
  1. Ensure type‑hint completeness and be explicit about hash semantics

    • To satisfy your “all functions annotated” goal, add a return type to __hash__:
  • def hash(self):
  •    return hash(self.data)
    
  • def hash(self) -> int:
  •    return hash(self.data)
    
    
    - Because `__hash__` delegates to `hash(self.data)`, any node whose `value` is not hashable (e.g. a list) becomes unhashable even if a custom `key` is provided. If your intended contract is “nodes are hashable only when their values are hashable”, this is fine but should be assumed/documented. If you instead want to support unhashable `value` with a custom `key`, the hash/equality design may need to be revisited (e.g. basing both on `key`).
    
    
datastructures/linked_lists/singly_linked_list/test_singly_linked_revese.py (2)

29-40: Remove duplicate test case.

Lines 29-34 and 35-40 contain identical test data. Remove one to avoid redundant test execution.

             (
                 [103, 7, 10, -9, 105, 67, 31, 63],
                 1,
                 8,
                 [63, 31, 67, 105, -9, 10, 7, 103],
             ),
-            (
-                [103, 7, 10, -9, 105, 67, 31, 63],
-                1,
-                8,
-                [63, 31, 67, 105, -9, 10, 7, 103],
-            ),

78-89: Remove duplicate test case (same issue as above).

Lines 78-83 and 84-89 are identical duplicates.

datastructures/linked_lists/singly_linked_list/single_linked_list.py (2)

14-18: Consider removing redundant class-level head attribute.

The class attribute head: Optional[SingleNode] = None is shadowed by the instance attribute set in the parent's __init__. While not a bug (since super().__init__() assigns to self.head), the class-level declaration is unnecessary and could be misleading about shared state.

 class SinglyLinkedList(LinkedList):
     """
     Implementation of a SinglyLinked List
     """

-    head: Optional[SingleNode] = None
-
     def __init__(self):
         # noinspection PyCompatibility
         super().__init__()

92-94: Stub method: insert_before_node is not implemented.

This overrides an abstract method from LinkedList but provides no implementation. Either implement the method or document why it's intentionally left as a stub.

Would you like me to generate an implementation for insert_before_node?

algorithms/sliding_window/repeated_dna_sequences/test_repeated_dna_sequences.py (1)

5-41: Consider adding tests for invalid characters and boundary lengths.

The current suites validate core behavior well for both implementations. To lock in edge‑case semantics, consider adding:

  • A case where dna_sequence contains an invalid character (e.g., "ACGTXACGTX") asserting that find_repeated_dna_sequences raises ValueError.
  • Boundary cases for both functions:
    • len(dna_sequence) < 10 (e.g., "ACGTACGTA") returning [].
    • len(dna_sequence) == 10 returning [] as there cannot be any repeats.

This will better document and protect the public API behavior if the internals change later.

Also applies to: 43-79

algorithms/sliding_window/repeated_dna_sequences/__init__.py (2)

4-30: Naive implementation is correct; loop bound could be tightened if desired.

The naive solution correctly identifies repeated 10‑length substrings using seen and result_set, with an early return for len(dna_sequence) <= 10. If you want to make the intent a bit clearer and avoid generating short trailing substrings, you could iterate only up to len(dna_sequence) - 9 and drop the len(subsequence) < 10 guard, but that’s purely cosmetic here.


33-91: Rolling-hash implementation looks solid; consider consistent invalid-input handling across both APIs.

The rolling‑hash version is well‑structured:

  • Base‑4 encoding and initial hash computation are correct.
  • Early return for short inputs avoids unnecessary work.
  • Sliding window update (old * 4) - left * 4^10 + right with precomputed a_k is consistent with the polynomial hash.
  • Result accumulation via output set ensures uniqueness, matching the naive behavior.

One behavioral difference is that this function raises ValueError on invalid characters while find_repeated_dna_sequences_naive silently accepts any input. Since the problem constraints already restrict inputs to A/C/G/T, this isn’t a correctness bug, but you may want to either:

  • Mirror the same validation in the naive implementation, or
  • Document that only the optimized version enforces input validation.
algorithms/graphs/course_schedule/README.md (2)

54-58: Clarify and split the combined constraints bullet

The last constraints bullet currently concatenates several conditions into one sentence, which is hard to read and easy to misparse.

Consider splitting it into separate bullets, e.g.:

-- prerequisites[i].length == 2 0 <= ai, bi < numCourses ai != bi All the pairs [ai, bi] are distinct.
+- prerequisites[i].length == 2
+- 0 <= ai, bi < numCourses
+- ai != bi
+- All the pairs [ai, bi] are distinct.

This keeps the spec aligned with the standard problem statement and makes it much clearer for readers.


62-107: Tighten terminology and complexity wording in solution breakdowns

A few minor doc nits that would improve precision:

  • Use the standard hyphenated term “Depth-First Search” in the “Solution 1 Breakdown” heading.
  • When describing recursion space, it’s more accurate to say the call stack uses O(V) space (proportional to the longest path in vertices) rather than O(E).
  • In the Solution 2 space-complexity section, “storing all of the edges” can be shortened to “storing all the edges” per the style hint.

These are small wording tweaks but make the explanations more precise and polished. Based on static analysis hints.

Also applies to: 139-147

algorithms/graphs/course_schedule/__init__.py (2)

9-73: DFS-based topological sort looks good; align docstring and input handling

The DFS + color-marking implementation for find_order is standard and matches the expected behavior for the course-schedule problem.

Two small points:

  • The docstring currently says “a list of tuples … source and destination of the prerequisite”, while the type hint is List[List[int]] and the code treats each pair as [course, prerequisite] (for destination, source in prerequisites). It would be clearer to spell this out explicitly, e.g. “list of [course, prerequisite] pairs where [a, b] means you must take b before a.”
  • Right now you rely on the problem constraints that 0 <= course_id < num_courses. If any prerequisite has a destination outside that range but a source inside it (e.g. [4, 1] with num_courses = 3), color[neighbour] will raise a KeyError. If you want the function to be robust to such inputs (or to match the behavior you’re testing with out-of-range IDs), consider filtering when building the adjacency list:
-    for destination, source in prerequisites:
-        adjacency_list[source].append(destination)
+    for destination, source in prerequisites:
+        if 0 <= source < num_courses and 0 <= destination < num_courses:
+            adjacency_list[source].append(destination)

Alternatively, document the index-range precondition more explicitly so callers know invalid course IDs are undefined behavior.


76-124: Kahn’s algorithm implementation is solid; clarify prerequisites shape and optional robustness

The can_finish implementation cleanly follows Kahn’s algorithm (in-degree tracking + BFS) and the early return for num_courses <= 0 is straightforward.

A few minor improvements:

  • As with find_order, the docstring describes “a list of tuples … source and destination”, but the actual structure is List[List[int]] with pairs interpreted as [course, prerequisite]. Making that explicit will reduce confusion.
  • Your tests include a case like num_courses = 3, prerequisites = [[1, 0], [2, 1], [4, 3]], where some course IDs are outside [0, num_courses). The current implementation effectively ignores those extra nodes when deciding feasibility, which matches the test, but the behavior for other out-of-range patterns (e.g. [4, 1]) is less obvious. If you want consistent semantics, you could either:
    • Filter to only edges fully within range:

  • for child, parent in prerequisites:
  •    if parent in graph:
    
  •        graph[parent].append(child)
    
  •    else:
    
  •        graph[parent] = [child]
    
  •    if child in in_degree:
    
  •        in_degree[child] += 1
    
  •    else:
    
  •        in_degree[child] = 1
    
  • for child, parent in prerequisites:
  •    if 0 <= parent < num_courses and 0 <= child < num_courses:
    
  •        graph[parent].append(child)
    
  •        in_degree[child] += 1
    
    
    - Or explicitly document that course IDs are expected to be in `[0, num_courses)` and that anything else is outside the function’s contract.
    
    

Either approach would make the behavior clearer to future readers.

algorithms/graphs/course_schedule/test_course_schedule.py (3)

13-18: Relax find_order expectations where multiple topological orders are valid

In test_2, the graph admits more than one valid topological order (e.g. [0, 1, 2, 3] and [0, 2, 1, 3]). Asserting a single fixed sequence makes the test brittle to benign implementation changes (e.g. different neighbor iteration order).

Consider asserting that the actual order is one of a small set of valid orders or, more generally, that it respects all prerequisite constraints (every edge u -> v has u before v), rather than enforcing one specific permutation.


41-46: Clarify intent of test_6’s comment with out-of-range course IDs

test_6 is a nice addition to check behavior when prerequisites mentions courses beyond num_courses. However, the inline comment:

expected = [0, 1, 2]  # or [0,1,2,3,4]

is misleading: with num_courses = 3, the current API and implementation will never return a list longer than num_courses.

If the intent is to assert that extra prerequisites for out-of-range courses are ignored, you might update the comment to say that explicitly and drop the [0,1,2,3,4] alternative.


49-85: Add a can_finish test for num_courses <= 0

can_finish has an explicit early return for num_courses <= 0, but there’s no test covering that path.

Consider adding a simple case like:

self.assertTrue(can_finish(num_courses=0, prerequisites=[]))

to lock in the intended behavior.

Comment on lines 9 to 10
- uses: actions/checkout@v5
- uses: actions/cache@v4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/pre-commit.yml | head -30

Repository: BrianLusina/PythonSnips

Length of output: 852


🌐 Web query:

actions/setup-python latest version GitHub Actions

💡 Result:

The latest release is actions/setup-python v6.1.0 — published Nov 25, 2025. [1][2]

Sources:
[1] actions/setup-python Releases (GitHub).
[2] actions/setup-python README / Marketplace page (GitHub).


🌐 Web query:

psf/black latest version GitHub Actions

💡 Result:

Use the official action at psf/black@stable to track the latest released Black from PyPI; or pin to a specific release like psf/black@24.4.0 to lock the version. [1][2]

Sources:

  • psf/black releases (example: 24.4.0). [1]
  • Usage: psf/black@stable (official GitHub Action / docs/examples). [2]

Update remaining outdated actions for consistency.

The checkout@v5 and cache@v4 upgrades are good, but also update:

  • actions/setup-python@v2 → current latest is v6.1.0
  • psf/black@21.4b0 → use psf/black@stable or pin to a recent version like psf/black@24.4.0
🤖 Prompt for AI Agents
.github/workflows/pre-commit.yml around lines 9 to 10: the workflow updates
checkout and cache actions but leaves older pins for setup-python and psf/black;
change uses: actions/setup-python@v2 to actions/setup-python@v6 or
actions/setup-python@v6.1.0 (or the latest v6.x tag) and change uses:
psf/black@21.4b0 to either psf/black@stable or pin to a recent release such as
psf/black@24.4.0 so all actions are consistent and up-to-date.

Comment on lines 40 to 43
def test_frog_position(self, n, edges, t, target, expected):
actual = frog_position_2(n, edges, t, target)
actual_rounded = round(actual, 5)
self.assertEqual(expected, actual_rounded)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix duplicate test method name.

Both test methods are named test_frog_position. The second definition (Line 40) overrides the first (Line 21), so frog_position is never tested—only frog_position_2 runs.

Apply this diff:

-    def test_frog_position(self, n, edges, t, target, expected):
+    def test_frog_position_2(self, n, edges, t, target, expected):
         actual = frog_position_2(n, edges, t, target)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_frog_position(self, n, edges, t, target, expected):
actual = frog_position_2(n, edges, t, target)
actual_rounded = round(actual, 5)
self.assertEqual(expected, actual_rounded)
def test_frog_position_2(self, n, edges, t, target, expected):
actual = frog_position_2(n, edges, t, target)
actual_rounded = round(actual, 5)
self.assertEqual(expected, actual_rounded)
🤖 Prompt for AI Agents
In
algorithms/graphs/frog_position_after_t_seconds/test_frog_position_after_t_seconds.py
around lines 40 to 43, the second test method is named test_frog_position which
overrides the earlier test and prevents frog_position from being run; rename
this second method to a unique name such as test_frog_position_2 (or similar) so
both tests run independently and keep the body calling frog_position_2 and the
existing assertions unchanged.

Comment on lines 68 to 90
def insert_after_node(self, prev_node: Any, data: Any):
if self.is_empty():
raise EmptyLinkedList("LinkedList has no Nodes")
if not prev_node:
raise ValueError("Prev Node can not be None")
if not data:
raise ValueError("Data to insert can not be None")

prev_node = (
prev_node if isinstance(prev_node, SingleNode) else SingleNode(prev_node)
)
node_to_insert = SingleNode(data)

current = self.head

# traverse the linked list until we find the node to insert
while current.next:
if current == prev_node:
node_to_insert.next = prev_node.next
prev_node.next = node_to_insert
# we have inserted the node, now we can exit
break
current = current.next
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Logic bug: insert_after_node will fail to find the target node.

When prev_node is not a SingleNode, line 77 creates a new SingleNode. The comparison current == prev_node at line 85 uses Node.__eq__ which compares keys. Since the newly created node isn't in the list and has a freshly computed key, it will never match existing nodes.

Additionally, line 73 rejects falsy values like 0 or "" which may be valid data.

     def insert_after_node(self, prev_node: Any, data: Any):
         if self.is_empty():
             raise EmptyLinkedList("LinkedList has no Nodes")
         if not prev_node:
             raise ValueError("Prev Node can not be None")
-        if not data:
+        if data is None:
             raise ValueError("Data to insert can not be None")

-        prev_node = (
-            prev_node if isinstance(prev_node, SingleNode) else SingleNode(prev_node)
-        )
         node_to_insert = SingleNode(data)

         current = self.head

         # traverse the linked list until we find the node to insert
-        while current.next:
-            if current == prev_node:
+        while current:
+            # Compare by data value, not node identity
+            target_data = prev_node.data if isinstance(prev_node, SingleNode) else prev_node
+            if current.data == target_data:
                 node_to_insert.next = current.next
                 current.next = node_to_insert
-                # we have inserted the node, now we can exit
-                break
+                return
             current = current.next
🤖 Prompt for AI Agents
In datastructures/linked_lists/singly_linked_list/single_linked_list.py around
lines 68 to 90, the method creates a new SingleNode for prev_node and then
compares nodes by equality (which uses node keys), so it will never match an
existing node; also the checks use falsy checks that reject valid values like 0
or "". Fix by: accept prev_node as either an actual SingleNode instance or a
value to search for, do not construct a new node to represent prev_node, change
validations to use "is None" (e.g. if prev_node is None or if data is None) so
falsy-but-valid values are allowed, iterate the list using "current" (while
current) and compare either node identity (if prev_node is a SingleNode) or
node.data == prev_node (if prev_node is a value), insert when matched, and raise
a clear error if the target prev_node is not found.

Comment on lines 134 to 147
while current is not None:
for _ in range(position):
current = current.next

if current is None:
raise ValueError(
f"Invalid position {position} found, reached end of list"
)

current.data = current.next.data if current.next else None
current.next = current.next.next if current.next else None

return self.head
return None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Edge-case bug: deleting the last node leaves a "zombie" node with None data.

When position points to the last node, current.next is None. The code sets current.data = None and current.next = None, but the node remains in the list with null data instead of being removed.

The mixed while/for loop structure is also confusing. Consider a cleaner traversal approach.

         while current is not None:
-            for _ in range(position):
+            for _ in range(position - 1):
                 current = current.next

                 if current is None:
                     raise ValueError(
                         f"Invalid position {position} found, reached end of list"
                     )

-            current.data = current.next.data if current.next else None
-            current.next = current.next.next if current.next else None
+            if current.next is None:
+                raise ValueError(f"Invalid position {position} found, reached end of list")

+            deleted = current.next
+            current.next = current.next.next
-            return self.head
+            return deleted
         return None
🤖 Prompt for AI Agents
In datastructures/linked_lists/singly_linked_list/single_linked_list.py around
lines 134-147, the deletion logic sets the target node's data to None when
deleting the last node instead of removing the node, and the mixed while/for
traversal is confusing; change to a clearer traversal: handle position==0 by
updating self.head to head.next (or None), otherwise iterate with a single loop
to stop at the node before the target (keeping a prev and current pointer),
validate bounds and raise ValueError if position is out of range, then remove
the target by setting prev.next = target.next (so the last node is removed
rather than nulled), and return self.head. Ensure you do not set node.data =
None anywhere for deletion.

Comment on lines 281 to 290
def shift(self):
"""
Since this is a singly linked list, this will have to make the head's next to the position of head
:return: deleted node
"""
# store the head node
to_del = self.head
# replace the head with the next value in the LinkedList
self.head = self.head.next
return to_del
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential AttributeError when shifting an empty list.

If self.head is None, line 289 (self.head.next) raises AttributeError. Add an empty-list guard.

     def shift(self):
         """
         Since this is a singly linked list, this will have to make the head's next to the position of head
         :return: deleted node
         """
+        if not self.head:
+            return None
         # store the head node
         to_del = self.head
         # replace the head with the next value in the LinkedList
         self.head = self.head.next
         return to_del
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def shift(self):
"""
Since this is a singly linked list, this will have to make the head's next to the position of head
:return: deleted node
"""
# store the head node
to_del = self.head
# replace the head with the next value in the LinkedList
self.head = self.head.next
return to_del
def shift(self):
"""
Since this is a singly linked list, this will have to make the head's next to the position of head
:return: deleted node
"""
if not self.head:
return None
# store the head node
to_del = self.head
# replace the head with the next value in the LinkedList
self.head = self.head.next
return to_del
🤖 Prompt for AI Agents
In datastructures/linked_lists/singly_linked_list/single_linked_list.py around
lines 281 to 290, the shift method assumes self.head is non-None and will raise
AttributeError when called on an empty list; add an early guard that returns
None (or a clear sentinel) if self.head is None, otherwise proceed to store
to_del, set self.head to to_del.next, decrement/update list length, and if the
removed node was also the tail (i.e. list became empty) ensure self.tail is set
to None before returning the removed node.

Comment on lines 303 to 341
def find_closest_value_in_bst(self, target: T) -> Optional[BinaryTreeNode]:
"""
Finds the closest value in the binary search tree to the given target value.
Args:
target T: Value to search for
Returns:
Node with the closest value to the target
"""
# edge case for empty nodes, if none is provided, we can't find a value that is close to the target
if not self.root:
return None

# if the node's data is the target, exit early by returning it
if self.root.data == target:
return self.root

# this keeps track of the minimum on both the left and the right
closest_node = self.root
min_diff = abs(target - self.root.data)
current = self.root

# while the queue is not empty, we pop off nodes from the queue and check for their values
while current:
current_diff = abs(target - self.root.data)

if current_diff < min_diff:
min_diff = current_diff
closest_node = current

if current.data == target:
return current

if target < current.data:
current = current.left
else:
current = current.right

return closest_node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix critical bug in closest value calculation.

Line 327 incorrectly uses self.root.data instead of current.data when calculating the difference. This means the method always compares against the root node instead of the current node during traversal, producing incorrect results.

Apply this fix:

     # while the queue is not empty, we pop off nodes from the queue and check for their values
     while current:
-        current_diff = abs(target - self.root.data)
+        current_diff = abs(target - current.data)

         if current_diff < min_diff:
             min_diff = current_diff
             closest_node = current
🤖 Prompt for AI Agents
In datastructures/trees/binary/search_tree/__init__.py around lines 303 to 341,
the closest-value loop mistakenly computes current_diff using self.root.data
instead of current.data; change the calculation to use current.data so each
visited node's value is compared to target, leaving the rest of the traversal
and updates (min_diff, closest_node, equality check and left/right moves)
unchanged.

Comment on lines 393 to 405
def is_complete_helper(root, idx: int) -> bool:
if root is None:
return True

if idx >= node_count:
return False

return is_complete_helper(root.left, 2 * idx + 1) and is_complete_helper(
root.right, 2 * index + 2
)

index = 0
return is_complete_helper(self.root, index)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: is_complete_helper uses wrong variable for right child index.

Line 401 uses index (the outer variable initialized to 0 at line 404) instead of idx (the function parameter). This causes incorrect index calculation for right children, breaking the completeness check.

         def is_complete_helper(root, idx: int) -> bool:
             if root is None:
                 return True

             if idx >= node_count:
                 return False

             return is_complete_helper(root.left, 2 * idx + 1) and is_complete_helper(
-                root.right, 2 * index + 2
+                root.right, 2 * idx + 2
             )

         index = 0
         return is_complete_helper(self.root, index)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def is_complete_helper(root, idx: int) -> bool:
if root is None:
return True
if idx >= node_count:
return False
return is_complete_helper(root.left, 2 * idx + 1) and is_complete_helper(
root.right, 2 * index + 2
)
index = 0
return is_complete_helper(self.root, index)
def is_complete_helper(root, idx: int) -> bool:
if root is None:
return True
if idx >= node_count:
return False
return is_complete_helper(root.left, 2 * idx + 1) and is_complete_helper(
root.right, 2 * idx + 2
)
index = 0
return is_complete_helper(self.root, index)
🤖 Prompt for AI Agents
In datastructures/trees/binary/tree/binary_tree.py around lines 393 to 405, the
helper uses the outer variable "index" instead of the parameter "idx" when
computing the right child index which breaks the completeness check; change the
right-child calculation to use idx (i.e., 2 * idx + 2) and keep calling
is_complete_helper(self.root, 0) (you can remove or keep the outer "index"
variable but ensure the helper consistently uses its idx parameter for both left
and right recursions).

Comment on lines 444 to 459
def is_height_balanced(node: BinaryTreeNode) -> bool:
left_height = 0
right_height = 0

if node is None:
return True

l = is_height_balanced(node.left)
r = is_height_balanced(node.right)

if abs(left_height - right_height) <= 1:
return l and r

return False

return is_height_balanced(self.root)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug: is_height_balanced never computes actual heights.

left_height and right_height remain 0—they're never updated from child heights. The condition abs(left_height - right_height) <= 1 always evaluates to abs(0 - 0) <= 1, which is always True. This method will incorrectly report unbalanced trees as balanced.

The helper should return height (or -1 for unbalanced) rather than a boolean:

     def is_balanced(self) -> bool:
         if self.root is None:
             return True

-        def is_height_balanced(node: BinaryTreeNode) -> bool:
-            left_height = 0
-            right_height = 0
-
+        def check_height(node: BinaryTreeNode) -> int:
+            """Returns height if balanced, -1 if unbalanced."""
             if node is None:
-                return True
-
-            l = is_height_balanced(node.left)
-            r = is_height_balanced(node.right)
+                return 0

-            if abs(left_height - right_height) <= 1:
-                return l and r
+            left_height = check_height(node.left)
+            if left_height == -1:
+                return -1
+
+            right_height = check_height(node.right)
+            if right_height == -1:
+                return -1

-            return False
+            if abs(left_height - right_height) > 1:
+                return -1

-        return is_height_balanced(self.root)
+            return 1 + max(left_height, right_height)
+
+        return check_height(self.root) != -1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def is_height_balanced(node: BinaryTreeNode) -> bool:
left_height = 0
right_height = 0
if node is None:
return True
l = is_height_balanced(node.left)
r = is_height_balanced(node.right)
if abs(left_height - right_height) <= 1:
return l and r
return False
return is_height_balanced(self.root)
def check_height(node: BinaryTreeNode) -> int:
"""Returns height if balanced, -1 if unbalanced."""
if node is None:
return 0
left_height = check_height(node.left)
if left_height == -1:
return -1
right_height = check_height(node.right)
if right_height == -1:
return -1
if abs(left_height - right_height) > 1:
return -1
return 1 + max(left_height, right_height)
return check_height(self.root) != -1
🤖 Prompt for AI Agents
In datastructures/trees/binary/tree/binary_tree.py around lines 444 to 459, the
is_height_balanced helper never computes or returns child heights (left_height
and right_height stay 0) so the balance check is wrong; change the helper to
return an integer height (or -1 to indicate unbalanced) instead of a boolean:
recursively call the helper to get left_height and right_height, if either is -1
return -1, if abs(left_height - right_height) > 1 return -1, otherwise return
max(left_height, right_height) + 1; then have the public
is_height_balanced(self) call the helper and treat a -1 result as unbalanced and
any non-negative result as balanced.

Comment on lines 46 to 47
if index is not None:
curr.index = min(curr.index or float("inf"), index)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: Index 0 is treated as falsy, causing incorrect behavior.

When curr.index is 0, the expression curr.index or float("inf") evaluates to float("inf") because 0 is falsy in Python. This means index 0 would be incorrectly overwritten by any subsequent index.

Apply this diff to fix the issue:

-            if index is not None:
-                curr.index = min(curr.index or float("inf"), index)
+            if index is not None:
+                curr.index = min(curr.index if curr.index is not None else float("inf"), index)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if index is not None:
curr.index = min(curr.index or float("inf"), index)
if index is not None:
curr.index = min(curr.index if curr.index is not None else float("inf"), index)
🤖 Prompt for AI Agents
In datastructures/trees/trie/trie.py around lines 46-47, the code uses
"curr.index or float('inf')" which treats 0 as falsy and will incorrectly
replace an existing index 0; change the expression to use an explicit None check
(e.g. use curr.index if curr.index is not None else float('inf')) so min()
receives the correct existing index value and 0 is preserved.

Comment on lines 51 to 73
def search(self, word: str) -> List[str]:
if len(word) == 0:
return []

curr = self.root

for char in word:
if char in curr.children:
curr = curr.children[char]
else:
return []

output = []

def dfs(node: TrieNode, prefix: str) -> None:
if node.is_end:
output.append((prefix + "".join(node.children.keys())))

for child in node.children.values():
dfs(child, prefix + "".join(node.children.keys()))

dfs(curr, word[:-1])
return output
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical bug in DFS logic: Incorrect word reconstruction.

The search method has multiple issues:

  1. Line 67: "".join(node.children.keys()) concatenates all children keys together instead of building the actual word path.
  2. Line 70: The recursive call joins all children keys rather than passing the specific character for each child.
  3. Line 72: word[:-1] drops the last character of the search prefix unnecessarily.

For a trie with words "cat", "car", the current logic would produce malformed results like "catr" instead of proper words.

Apply this diff to fix the DFS logic:

     def search(self, word: str) -> List[str]:
         if len(word) == 0:
             return []
 
         curr = self.root
 
         for char in word:
             if char in curr.children:
                 curr = curr.children[char]
             else:
                 return []
 
         output = []
 
-        def dfs(node: TrieNode, prefix: str) -> None:
+        def dfs(node: TrieNode, current_word: str) -> None:
             if node.is_end:
-                output.append((prefix + "".join(node.children.keys())))
+                output.append(current_word)
 
-            for child in node.children.values():
-                dfs(child, prefix + "".join(node.children.keys()))
+            for char, child in node.children.items():
+                dfs(child, current_word + char)
 
-        dfs(curr, word[:-1])
+        dfs(curr, word)
         return output
🤖 Prompt for AI Agents
In datastructures/trees/trie/trie.py around lines 51 to 73, the DFS builds words
incorrectly: replace the joins and wrong prefix handling by using the specific
child character when recursing and when appending completed words, and start the
DFS with the full search prefix (not word[:-1]); specifically, iterate children
as (char, child), on node.is_end append the current prefix, and recurse with
dfs(child, prefix + char), and invoke dfs(curr, word) after locating the prefix
node.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (11)
DIRECTORY.md (1)

214-226: Adjust Graphs > Edge / Undirected indentation to match other Datastructures entries and clear MD007.

This block is still over‑indented relative to the rest of the “Datastructures” section, and markdownlint flags MD007 on these lines. Reusing the earlier suggestion and extending it to the new “Undirected” subsection, you can normalize as:

-  * Graphs
-    * Edge
-      * [Edge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge.py)
-      * [Edge Directed](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_directed.py)
-      * [Edge Hyper](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_hyper.py)
-      * [Edge Self](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_self.py)
-      * [Edge Type](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_type.py)
-      * [Edge Undirected](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_undirected.py)
-    * [Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/graph.py)
-    * [Test Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/test_vertex.py)
-    * Undirected
-      * [Test Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/undirected/test_utils.py)
-      * [Undirected Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/undirected/undirected_graph.py)
-      * [Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/undirected/utils.py)
-    * [Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/vertex.py)
+  * Graphs
+  * Edge
+    * [Edge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge.py)
+    * [Edge Directed](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_directed.py)
+    * [Edge Hyper](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_hyper.py)
+    * [Edge Self](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_self.py)
+    * [Edge Type](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_type.py)
+    * [Edge Undirected](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/edge/edge_undirected.py)
+  * [Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/graph.py)
+  * [Test Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/test_vertex.py)
+  * Undirected
+    * [Test Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/undirected/test_utils.py)
+    * [Undirected Graph](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/undirected/undirected_graph.py)
+    * [Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/undirected/utils.py)
+  * [Vertex](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/graphs/vertex.py)

This brings these bullets to the same levels as other subsections (e.g., Arrays, Dicts) and resolves the MD007 indentation errors.

datastructures/graphs/edge/edge_hyper.py (1)

14-31: Fix HyperEdge identifier default and EdgeType to avoid subtle bugs.

Two issues here:

  1. identifier: AnyStr = uuid4() evaluates uuid4() at import time, so all HyperEdge instances constructed without an explicit identifier share the same id.
  2. edge_type() returns EdgeType.SELF instead of the appropriate hyper edge enum, which breaks type-based dispatch.

A minimal, consistent fix:

-from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any
-from uuid import uuid4
+from typing import Union, Dict, Optional, Generic, TypeVar, List, Any
+from uuid import uuid4, UUID
@@
-    def __init__(
-        self,
-        nodes: List[Any],
-        weight: Optional[Union[int, float]] = None,
-        properties: Optional[Dict[str, Any]] = None,
-        identifier: AnyStr = uuid4(),
-    ):
-        super().__init__(weight, properties, identifier)
+    def __init__(
+        self,
+        nodes: List[Any],
+        weight: Optional[Union[int, float]] = None,
+        properties: Optional[Dict[str, Any]] = None,
+        identifier: Optional[UUID] = None,
+    ):
+        super().__init__(weight, properties, identifier or uuid4())
@@
-    def edge_type(self) -> EdgeType:
-        return EdgeType.SELF
+    def edge_type(self) -> EdgeType:
+        return EdgeType.HYPER_UNDIRECTED

(Use HYPER_DIRECTED instead if that better matches how you model hyper-edges elsewhere.)

This aligns HyperEdge with the rest of the edge implementations and avoids the shared-id pitfall.

datastructures/graphs/edge/edge_directed.py (1)

15-34: Avoid uuid4() as a default argument in DirectedEdge and align identifier typing.

Like HyperEdge, this constructor uses identifier: AnyStr = uuid4(), which binds a single UUID at import time and reuses it for all edges without an explicit id.

Recommend switching to late binding and a UUID-oriented type:

-from typing import AnyStr, Union, Dict, Optional, Generic, TypeVar, List, Any
-from uuid import uuid4
+from typing import Union, Dict, Optional, Generic, TypeVar, List, Any
+from uuid import uuid4, UUID
@@
-    def __init__(
-        self,
-        source: Any,
-        destination: Any,
-        weight: Optional[Union[int, float]] = None,
-        properties: Optional[Dict[str, Any]] = None,
-        identifier: AnyStr = uuid4(),
-    ):
-        super().__init__(weight, properties, identifier)
+    def __init__(
+        self,
+        source: Any,
+        destination: Any,
+        weight: Optional[Union[int, float]] = None,
+        properties: Optional[Dict[str, Any]] = None,
+        identifier: Optional[UUID] = None,
+    ):
+        super().__init__(weight, properties, identifier or uuid4())

This keeps identifiers unique by default and consistent with the rest of the edge API.

datastructures/graphs/vertex.py (1)

13-31: Vertex id, hashability, edge API usage, and degree calculations need fixes to work with the new Edge model.

There are several coupled issues here that will break graphs at runtime:

  1. Default identifier uses uuid4() in the signature.
    identifier: Any = uuid4() binds a single UUID at import time; all vertices created without an explicit id share it.

  2. Vertex is not hashable but is used as a dict key.
    You define __eq__ but not __hash__, so instances become unhashable. Graph uses Vertex in self.adjacency_list[...], which will raise TypeError.

  3. self.edges is computed once and can go stale.
    It’s set to incoming_edges.union(outgoing_edges) in __init__, so later mutations of either set aren’t reflected.

  4. adjacent_vertices key typing is too narrow.
    Keys are other.id (often a UUID), but the type is Dict[str, "Vertex"].

  5. degree, in_degree, and out_degree don’t match the new Edge API.

    • degree returns 0 whenever either incoming or outgoing is empty, even if one side has edges.
    • It checks if not edge.weight, which treats weight=0 as “unweighted” and ignores Edge.is_unweighted().
    • in_degree / out_degree use edge.type, edge.node_two, and edge.node_one, but the new edges expose edge_type() and vertices() instead (and DirectedEdge uses source / destination).

A consolidated refactor to address these:

-from typing import Any, Set, Dict, Optional, Generic, TypeVar, List
-from .edge import Edge, EdgeType
+from typing import Any, Set, Dict, Optional, Generic, TypeVar, List
+from .edge import Edge, EdgeType
@@
     def __init__(
         self,
         data: T,
-        identifier: Any = uuid4(),
+        identifier: Any = None,
         properties: Optional[Dict[str, Any]] = None,
         incoming_edges: Optional[Set[Edge]] = None,
         outgoing_edges: Optional[Set[Edge]] = None,
     ):
         if outgoing_edges is None:
             outgoing_edges = set()
         if incoming_edges is None:
             incoming_edges = set()
-        self.id = identifier
+        self.id = uuid4() if identifier is None else identifier
         self.data = data
         self.incoming_edges = incoming_edges
         self.outgoing_edges = outgoing_edges
-        self.edges = self.incoming_edges.union(self.outgoing_edges)
-        self.adjacent_vertices: Dict[str, "Vertex"] = {}
+        self.adjacent_vertices: Dict[Any, "Vertex"] = {}
         self.properties = properties
@@
-    def __eq__(self, other: "Vertex") -> bool:
-        return self.id == other.id
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, Vertex):
+            return NotImplemented
+        return self.id == other.id
+
+    def __hash__(self) -> int:
+        return hash(self.id)
@@
     @property
     def neighbours(self) -> List["Vertex"]:
@@
-        nodes = []
-        for vertex in self.adjacent_vertices.values():
-            nodes.append(vertex)
-
-        return nodes
+        return list(self.adjacent_vertices.values())
@@
     @property
     def degree(self) -> int:
@@
-        degrees = 0
-
-        if len(self.incoming_edges) == 0 or len(self.outgoing_edges) == 0:
-            return degrees
-
-        seen_edges: Set = set()
-
-        for edge in self.edges:
-            if edge not in seen_edges:
-                seen_edges.add(edge)
-                if not edge.weight:
-                    degrees += 1
-
-        return degrees
+        degrees = 0
+        if len(self.edges) == 0:
+            return degrees
+
+        seen_edges: Set[Edge] = set()
+        for edge in self.edges:
+            if edge not in seen_edges:
+                seen_edges.add(edge)
+                if edge.is_unweighted():
+                    degrees += 1
+
+        return degrees
@@
     @property
     def in_degree(self) -> int:
@@
-        in_degrees = 0
-        if len(self.edges) == 0:
-            return in_degrees
-
-        for edge in self.edges:
-            if edge.type == EdgeType.DIRECTED and edge.node_two == self:
-                in_degrees += 1
-
-        return in_degrees
+        in_degrees = 0
+        if len(self.edges) == 0:
+            return in_degrees
+
+        for edge in self.edges:
+            if edge.edge_type() == EdgeType.DIRECTED:
+                vertices = edge.vertices()
+                if len(vertices) >= 2 and vertices[1] is self:
+                    in_degrees += 1
+
+        return in_degrees
@@
     @property
     def out_degree(self) -> int:
@@
-        out_degrees = 0
-        if len(self.edges) == 0:
-            return out_degrees
-
-        for edge in self.edges:
-            if edge.type == EdgeType.DIRECTED and edge.node_one == self:
-                out_degrees += 1
-
-        return out_degrees
+        out_degrees = 0
+        if len(self.edges) == 0:
+            return out_degrees
+
+        for edge in self.edges:
+            if edge.edge_type() == EdgeType.DIRECTED:
+                vertices = edge.vertices()
+                if len(vertices) >= 1 and vertices[0] is self:
+                    out_degrees += 1
+
+        return out_degrees
+
+    @property
+    def edges(self) -> Set[Edge]:
+        return self.incoming_edges | self.outgoing_edges

With these changes:

  • Vertex ids are unique by default but still overridable.
  • Vertices become usable as keys in Graph.adjacency_list.
  • Degree / in_degree / out_degree work with the new Edge interface and avoid stale state.
  • Neighbour management remains adjacency-based and a bit cleaner.

Also applies to: 39-56, 58-67, 72-91, 93-109, 111-127

datastructures/graphs/edge/edge.py (1)

2-5: Fix Edge.identifier default & type to avoid shared UUID and mismatch

identifier currently has two problems:

  • uuid4() is evaluated at import time, so all Edge instances created without an explicit identifier will share the same UUID.
  • Annotation AnyStr does not match uuid4()’s UUID return type.

Given this is the base class, this breaks identity for all edge subclasses.

Consider updating imports and __init__ as follows:

-from typing import AnyStr, Union
-from .edge_type import EdgeType
-from typing import Any, Dict, Optional, Generic, TypeVar, List
-from uuid import uuid4
+from typing import Any, Dict, Optional, Generic, TypeVar, List, Union
+from uuid import UUID, uuid4
+from .edge_type import EdgeType
@@
     def __init__(
         self,
         weight: Optional[Union[int, float]] = None,
         properties: Optional[Dict[str, Any]] = None,
-        identifier: AnyStr = uuid4(),
+        identifier: Optional[UUID] = None,
     ):
-        self.id = identifier
+        self.id = identifier if identifier is not None else uuid4()
         self.weight = weight
         self.properties = properties

This keeps UUID generation per-instance and aligns the type annotation with actual values.

Also applies to: 15-23

datastructures/graphs/graph.py (6)

17-25: Fix adjacency list factory and node bookkeeping in __init__

Current constructor has several issues:

  • defaultdict(List[Vertex]) uses the typing alias as a factory; you want list to get fresh lists.
  • self.nodes is initialised empty and node_count is derived from that, ignoring any vertices present in the provided edge_list.
  • node_count then becomes stale as nodes are added later.

A safer pattern:

     def __init__(self, edge_list: List[Edge] = None):
         if edge_list is None:
             edge_list = []
         self.edge_list = edge_list
-        self.adjacency_list = defaultdict(List[Vertex])
-        self.__construct_adjacency_list()
-        self.nodes = []
-        self.node_count = len(self.nodes)
+        # maps Vertex -> List[Vertex]
+        self.adjacency_list = defaultdict(list)
+        self.nodes: List[Vertex] = []
+
+        # Seed nodes from existing edges
+        for edge in self.edge_list:
+            for vertex in edge.vertices():
+                if vertex not in self.nodes:
+                    self.nodes.append(vertex)
+
+        self.__construct_adjacency_list()

Also drop self.node_count entirely and use len(self.nodes) or if not self.nodes elsewhere (see topological_sorted_order).


26-37: add() misuses Vertex.neighbours and instantiates abstract Edge

add is currently broken:

  • Vertex.neighbours is a computed property; appending to it does not persist adjacency.
  • Edge is abstract and its constructor doesn’t accept node_one/node_two, so Edge(node_one=..., node_two=...) will fail.
  • self.nodes gets duplicate entries for the same vertex.

Leverage Vertex.add_adjacent_vertex and a concrete edge type, and avoid duplicates:

     def add(self, source_node: Vertex, destination_node: Vertex):
@@
-        source_node.neighbours.append(destination_node)
-        destination_node.neighbours.append(source_node)
-        edge = Edge(node_one=source_node, node_two=destination_node)
-        self.edge_list.append(edge)
-        self.adjacency_list[source_node].append(destination_node)
-        self.adjacency_list[destination_node].append(source_node)
-        self.nodes.append(source_node)
-        self.nodes.append(destination_node)
+        # Update vertex-level adjacency (undirected)
+        source_node.add_adjacent_vertex(destination_node)
+
+        # Create an undirected edge between the two vertices
+        edge = UndirectedEdge(node_one=source_node, node_two=destination_node)
+        self.edge_list.append(edge)
+
+        # Update graph-level adjacency
+        self.adjacency_list[source_node].append(destination_node)
+        self.adjacency_list[destination_node].append(source_node)
+
+        if source_node not in self.nodes:
+            self.nodes.append(source_node)
+        if destination_node not in self.nodes:
+            self.nodes.append(destination_node)

You’ll also need to import UndirectedEdge:

-from datastructures.graphs.edge import Edge
+from datastructures.graphs.edge import Edge, UndirectedEdge

7-8: Build adjacency correctly for directed vs undirected edges

__construct_adjacency_list assumes edge.node_one/edge.node_two only and ignores direction, so directed edges won’t be represented correctly and undirected edges are only stored in one direction.

Given the presence of DirectedEdge and UndirectedEdge, you can distinguish them explicitly:

-from datastructures.graphs.edge import Edge
+from datastructures.graphs.edge import Edge, UndirectedEdge, DirectedEdge
@@
     def __construct_adjacency_list(self):
         """
         Construct adjacency list
         """
-        for edge in self.edge_list:
-            self.adjacency_list[edge.node_one].append(edge.node_two)
+        for edge in self.edge_list:
+            if isinstance(edge, UndirectedEdge):
+                self.adjacency_list[edge.node_one].append(edge.node_two)
+                self.adjacency_list[edge.node_two].append(edge.node_one)
+            elif isinstance(edge, DirectedEdge):
+                self.adjacency_list[edge.source].append(edge.destination)

If you later support hyper-edges, this method will need to be extended to handle multiple incident vertices per edge.

Also applies to: 39-45


61-112: Rewrite topological_sorted_order (visited map, recursion target, abstract Stack)

Current implementation has multiple correctness issues:

  • Uses self.node_count and builds visited_nodes keyed by range(self.node_count) instead of actual Vertex instances.
  • Recurses with dfs(node) instead of dfs(neighbour).
  • Cycle check compares visited_nodes[node] instead of the neighbour’s state.
  • Instantiates abstract Stack and relies on a non-existent stack.stack attribute.

A DFS with color states and a plain list is simpler and correct:

     def topological_sorted_order(self) -> List[Vertex]:
@@
-        # Nothing to do here
-        if self.node_count == 0:
-            return []
-
-        is_possible = True
-        stack = Stack()
-
-        # By default all nodes are WHITE
-        visited_nodes = {node: white for node in range(self.node_count)}
-
-        def dfs(node: Vertex):
-            nonlocal is_possible
-
-            # Don't recurse further if we found a cycle already
-            if not is_possible:
-                return
-
-            # start recursion
-            visited_nodes[node] = gray
-
-            # Traverse on neighbouring nodes/vertices
-            if node in self.adjacency_list:
-                for neighbour in self.adjacency_list[node]:
-                    if visited_nodes[neighbour] == white:
-                        dfs(node)
-                    elif visited_nodes[node] == gray:
-                        # An Edge to a Gray vertex/node represents a cycle
-                        is_possible = False
-
-            # Recursion ends. We mark if as BLACK
-            visited_nodes[node] = black
-            stack.push(node)
-
-        for node in self.nodes:
-            # if the node is unprocessed, then call DFS on it
-            if visited_nodes[node] == white:
-                dfs(node)
-
-        return list(stack.stack) if is_possible else []
+        if not self.nodes:
+            return []
+
+        is_possible = True
+        visited = {}  # Vertex -> color
+        order: List[Vertex] = []
+
+        def dfs(node: Vertex):
+            nonlocal is_possible
+
+            if not is_possible:
+                return
+
+            visited[node] = gray
+
+            # Traverse neighbouring nodes/vertices
+            for neighbour in self.adjacency_list.get(node, []):
+                state = visited.get(neighbour, white)
+                if state == white:
+                    dfs(neighbour)
+                elif state == gray:
+                    # An Edge to a Gray vertex/node represents a cycle
+                    is_possible = False
+                    return
+
+            visited[node] = black
+            order.append(node)
+
+        for node in self.nodes:
+            if visited.get(node, white) == white:
+                dfs(node)
+
+        return list(reversed(order)) if is_possible else []

This removes the dependency on an abstract Stack implementation and operates directly on Vertex instances.


118-138: Fix exception type in remove() and adjacency bucket checked in is_connected()

Two issues here:

  • list.remove raises ValueError, not KeyError, so the current except KeyError will never fire.
  • is_connected checks self.adjacency_list[node_two] instead of node_one’s adjacency, so it answers the wrong question.

Suggested changes:

     def remove(self, node: Vertex) -> None:
@@
-        for _, cxns in self.adjacency_list.items():
-            try:
-                cxns.remove(node)
-            except KeyError:
-                pass
-
-        try:
-            del self.adjacency_list[node]
-        except KeyError:
-            pass
+        for _, cxns in self.adjacency_list.items():
+            try:
+                cxns.remove(node)
+            except ValueError:
+                # node not present in this adjacency bucket
+                pass
+
+        # Remove the node's own adjacency bucket if present
+        self.adjacency_list.pop(node, None)
@@
     def is_connected(self, node_one: Vertex, node_two: Vertex) -> bool:
-        return (
-            node_one in self.adjacency_list
-            and node_two in self.adjacency_list[node_two]
-        )
+        return node_two in self.adjacency_list.get(node_one, [])

You may also want to remove node from self.nodes and prune any incident edges in self.edge_list for full consistency, but that can be done as a follow-up.


140-167: Unify path methods to use Vertex keys (not .data) and correct path construction

All three path helpers (find_path, find_all_paths, find_shortest_path) share the same issues:

  • They mix Vertex instances and node.data when indexing adjacency_list and when tracking path, which currently stores a nested structure (path = [path] + [node_one]).
  • Membership checks like if node.data not in path compare a raw value against Vertex objects, so visited detection fails and recursion can misbehave.

They should consistently:

  • Treat adjacency_list as Dict[Vertex, List[Vertex]].
  • Store Vertex objects in path.
  • Use node_one == node_two (or id) for equality.

Concrete refactors:

     def find_path(
         self, node_one: Vertex, node_two: Vertex, path=None
@@
-        if path is None:
-            path = []
-        path = [path] + [node_one]
-
-        if node_one.data == node_two.data:
-            return path
-
-        if node_one.data not in self.adjacency_list:
-            return None
-
-        for node in self.adjacency_list[node_one]:
-            if node.data not in path:
-                new_path = self.find_path(node, node_two, path)
-
-                if new_path:
-                    return new_path
-
-        return None
+        if path is None:
+            path = []
+        path = path + [node_one]
+
+        if node_one == node_two:
+            return path
+
+        for neighbour in self.adjacency_list.get(node_one, []):
+            if neighbour not in path:
+                new_path = self.find_path(neighbour, node_two, path)
+                if new_path:
+                    return new_path
+        return None
@@
     def find_all_paths(
         self, node_one: Vertex, node_two: Vertex, path: List = None
@@
-        if path is None:
-            path = []
-        path = path + [node_one]
-
-        if node_one.data == node_two.data:
-            return [path]
-
-        if node_one.data not in self.adjacency_list:
-            return []
-
-        paths = []
-
-        for node in self.adjacency_list[node_one.data]:
-            if node not in path:
-                newpaths = self.find_all_paths(Vertex(node), node_two, path)
-                for newpath in newpaths:
-                    paths.append(newpath)
-
-        return paths
+        if path is None:
+            path = []
+        path = path + [node_one]
+
+        if node_one == node_two:
+            return [path]
+
+        paths: List[List[Vertex]] = []
+        for neighbour in self.adjacency_list.get(node_one, []):
+            if neighbour not in path:
+                new_paths = self.find_all_paths(neighbour, node_two, path)
+                paths.extend(new_paths)
+        return paths
@@
     def find_shortest_path(
         self, node_one: Vertex, node_two: Vertex, path: List = None
@@
-        if path is None:
-            path = []
-
-        path = path + [node_one]
-
-        if node_one.data == node_two.data:
-            return path
-
-        if node_one.data not in self.adjacency_list:
-            return None
-
-        shortest = None
-
-        for node in self.adjacency_list[node_one]:
-            if node.data not in path:
-                newpath = self.find_shortest_path(node, node_two, path)
-                if newpath:
-                    if not shortest or len(newpath) < len(shortest):
-                        shortest = newpath
-
-        return shortest
+        if path is None:
+            path = []
+
+        path = path + [node_one]
+
+        if node_one == node_two:
+            return path
+
+        shortest: Union[List[Vertex], None] = None
+
+        for neighbour in self.adjacency_list.get(node_one, []):
+            if neighbour not in path:
+                new_path = self.find_shortest_path(neighbour, node_two, path)
+                if new_path and (shortest is None or len(new_path) < len(shortest)):
+                    shortest = new_path
+
+        return shortest

This makes all three helpers consistent and usable on the same Vertex-keyed adjacency structure.

Also applies to: 169-196, 198-224

🧹 Nitpick comments (3)
datastructures/graphs/undirected/utils.py (1)

5-94: closest_node implementation looks correct; consider documenting assumptions and failure mode.

The BFS distance and parent-pointer path reconstruction are sound for a connected tree with nodes in [0, n-1], and the tie-breaking on index is implemented correctly. The only thing I’d suggest is:

  • Documenting that n is expected to be the number of nodes labeled 0..n-1 and that the graph is assumed connected / tree-like.
  • Optionally deciding how you want to handle invalid queries where start/end are disconnected or unreachable from target — right now those yield -1 in the result without an explicit error.

These are non-blocking API/UX clarifications rather than correctness issues.

DIRECTORY.md (2)

83-84: Fix indentation for “Frog Position After T Seconds” to satisfy MD007.

markdownlint expects these bullets to use 2 spaces for the parent and 4 for its child. You can align them with the surrounding sections like this:

-    * Frog Position After T Seconds
-      * [Test Frog Position After T Seconds](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/frog_position_after_t_seconds/test_frog_position_after_t_seconds.py)
+  * Frog Position After T Seconds
+    * [Test Frog Position After T Seconds](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/frog_position_after_t_seconds/test_frog_position_after_t_seconds.py)

This matches the “Algorithms” section style and clears the MD007 warnings for lines 83–84.


305-308: Reduce indentation for Binary Tree “Tree” entries to be consistent with other nested lists.

Lines 305–308 are indented 2 spaces more than markdownlint expects for that nesting level. Align them with the rest of the “Trees > Binary > Tree” hierarchy:

-      * Tree
-        * [Binary Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/binary_tree.py)
-        * [Test Binary Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree.py)
-        * [Test Binary Tree Deserialize](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_deserialize.py)
-        * [Test Binary Tree Min Camera Cover](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_min_camera_cover.py)
+      * Tree
+      * [Binary Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/binary_tree.py)
+      * [Test Binary Tree](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree.py)
+      * [Test Binary Tree Deserialize](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_deserialize.py)
+      * [Test Binary Tree Min Camera Cover](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/trees/binary/tree/test_binary_tree_min_camera_cover.py)

(Adjust the exact structure as you prefer, but the key is to use 6 spaces for items at this depth instead of 8 so MD007 passes.)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d851df5 and 0730fe9.

⛔ Files ignored due to path filters (3)
  • datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_1.png is excluded by !**/*.png
  • datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_2.png is excluded by !**/*.png
  • datastructures/graphs/undirected/images/examples/closest_node_to_path_in_tree_example_3.png is excluded by !**/*.png
📒 Files selected for processing (14)
  • DIRECTORY.md (3 hunks)
  • datastructures/graphs/edge/edge.py (1 hunks)
  • datastructures/graphs/edge/edge_directed.py (1 hunks)
  • datastructures/graphs/edge/edge_hyper.py (1 hunks)
  • datastructures/graphs/edge/edge_self.py (1 hunks)
  • datastructures/graphs/edge/edge_undirected.py (1 hunks)
  • datastructures/graphs/graph.py (1 hunks)
  • datastructures/graphs/test_vertex.py (1 hunks)
  • datastructures/graphs/undirected/README.md (1 hunks)
  • datastructures/graphs/undirected/__init__.py (1 hunks)
  • datastructures/graphs/undirected/test_utils.py (1 hunks)
  • datastructures/graphs/undirected/undirected_graph.py (1 hunks)
  • datastructures/graphs/undirected/utils.py (1 hunks)
  • datastructures/graphs/vertex.py (5 hunks)
✅ Files skipped from review due to trivial changes (1)
  • datastructures/graphs/undirected/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • datastructures/graphs/edge/edge_self.py
  • datastructures/graphs/test_vertex.py
  • datastructures/graphs/edge/edge_undirected.py
🧰 Additional context used
🧬 Code graph analysis (8)
datastructures/graphs/undirected/test_utils.py (1)
datastructures/graphs/undirected/utils.py (1)
  • closest_node (5-94)
datastructures/graphs/undirected/utils.py (1)
datastructures/graphs/graph.py (1)
  • find_path (140-167)
datastructures/graphs/graph.py (5)
datastructures/stacks/__init__.py (1)
  • Stack (7-48)
datastructures/graphs/vertex.py (2)
  • Vertex (8-127)
  • neighbours (59-69)
datastructures/graphs/edge/edge.py (1)
  • Edge (10-37)
datastructures/graphs/undirected/undirected_graph.py (2)
  • bfs_from_root_to_target (11-12)
  • bfs_from_node (14-15)
datastructures/graphs/undirected/utils.py (1)
  • find_path (31-70)
datastructures/graphs/undirected/undirected_graph.py (2)
datastructures/graphs/graph.py (3)
  • Graph (12-230)
  • bfs_from_root_to_target (47-52)
  • bfs_from_node (55-59)
datastructures/graphs/vertex.py (1)
  • Vertex (8-127)
datastructures/graphs/undirected/__init__.py (1)
datastructures/graphs/undirected/undirected_graph.py (1)
  • UndirectedGraph (6-15)
datastructures/graphs/edge/edge_directed.py (5)
datastructures/graphs/edge/edge.py (3)
  • edge_type (29-30)
  • Edge (10-37)
  • vertices (36-37)
datastructures/graphs/edge/edge_hyper.py (2)
  • edge_type (27-28)
  • vertices (30-31)
datastructures/graphs/edge/edge_self.py (2)
  • edge_type (27-28)
  • vertices (30-31)
datastructures/graphs/edge/edge_undirected.py (2)
  • edge_type (31-32)
  • vertices (34-35)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
datastructures/graphs/vertex.py (2)
datastructures/graphs/edge/edge.py (1)
  • Edge (10-37)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
datastructures/graphs/edge/edge.py (5)
datastructures/graphs/edge/edge_directed.py (2)
  • edge_type (30-31)
  • vertices (33-34)
datastructures/graphs/edge/edge_hyper.py (2)
  • edge_type (27-28)
  • vertices (30-31)
datastructures/graphs/edge/edge_self.py (2)
  • edge_type (27-28)
  • vertices (30-31)
datastructures/graphs/edge/edge_undirected.py (2)
  • edge_type (31-32)
  • vertices (34-35)
datastructures/graphs/edge/edge_type.py (1)
  • EdgeType (5-10)
🪛 markdownlint-cli2 (0.18.1)
DIRECTORY.md

83-83: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


84-84: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


214-214: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


215-215: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


216-216: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


217-217: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


218-218: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


219-219: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


220-220: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


221-221: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


222-222: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


223-223: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


224-224: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


225-225: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


226-226: Unordered list indentation
Expected: 4; Actual: 6

(MD007, ul-indent)


305-305: Unordered list indentation
Expected: 6; Actual: 8

(MD007, ul-indent)


306-306: Unordered list indentation
Expected: 6; Actual: 8

(MD007, ul-indent)


307-307: Unordered list indentation
Expected: 6; Actual: 8

(MD007, ul-indent)


308-308: Unordered list indentation
Expected: 6; Actual: 8

(MD007, ul-indent)

🔇 Additional comments (1)
datastructures/graphs/undirected/__init__.py (1)

1-4: Re-export of UndirectedGraph looks good.

The package-level import and __all__ definition are straightforward and keep the public surface clean (datastructures.graphs.undirected.UndirectedGraph).

Comment on lines +9 to +33
@parameterized.expand(
[
(3, [[0, 1], [1, 2]], [[0, 2, 1]], [1]),
(4, [[0, 1], [1, 2], [1, 3]], [[2, 3, 0]], [1]),
(
6,
[[0, 1], [0, 2], [0, 3], [0, 4], [0, 5]],
[[1, 5, 2], [2, 3, 4]],
[0, 0],
),
(
7,
[[0, 1], [0, 2], [0, 3], [1, 4], [2, 5], [2, 6]],
[[5, 3, 4], [5, 3, 6]],
[0, 2],
),
(3, [[0, 1], [1, 2]], [[0, 1, 2]], [1]),
(3, [[0, 1], [1, 2]], [[0, 0, 0]], [0]),
]
)
def test_closest_node(
self, n: int, edges: List[List[int]], query: List[List[int]], expected: int
):
actual = closest_node(n, edges, query)
self.assertEqual(expected, actual)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix type hint for expected to match closest_node’s return type.

The test cases pass lists (e.g., [1], [0, 0]), and closest_node returns List[int], but the signature currently uses expected: int. To keep hints accurate:

-    def test_closest_node(
-        self, n: int, edges: List[List[int]], query: List[List[int]], expected: int
-    ):
+    def test_closest_node(
+        self, n: int, edges: List[List[int]], query: List[List[int]], expected: List[int]
+    ):

Optional: consider renaming query to queries to reflect that it’s a list of query triples.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@parameterized.expand(
[
(3, [[0, 1], [1, 2]], [[0, 2, 1]], [1]),
(4, [[0, 1], [1, 2], [1, 3]], [[2, 3, 0]], [1]),
(
6,
[[0, 1], [0, 2], [0, 3], [0, 4], [0, 5]],
[[1, 5, 2], [2, 3, 4]],
[0, 0],
),
(
7,
[[0, 1], [0, 2], [0, 3], [1, 4], [2, 5], [2, 6]],
[[5, 3, 4], [5, 3, 6]],
[0, 2],
),
(3, [[0, 1], [1, 2]], [[0, 1, 2]], [1]),
(3, [[0, 1], [1, 2]], [[0, 0, 0]], [0]),
]
)
def test_closest_node(
self, n: int, edges: List[List[int]], query: List[List[int]], expected: int
):
actual = closest_node(n, edges, query)
self.assertEqual(expected, actual)
@parameterized.expand(
[
(3, [[0, 1], [1, 2]], [[0, 2, 1]], [1]),
(4, [[0, 1], [1, 2], [1, 3]], [[2, 3, 0]], [1]),
(
6,
[[0, 1], [0, 2], [0, 3], [0, 4], [0, 5]],
[[1, 5, 2], [2, 3, 4]],
[0, 0],
),
(
7,
[[0, 1], [0, 2], [0, 3], [1, 4], [2, 5], [2, 6]],
[[5, 3, 4], [5, 3, 6]],
[0, 2],
),
(3, [[0, 1], [1, 2]], [[0, 1, 2]], [1]),
(3, [[0, 1], [1, 2]], [[0, 0, 0]], [0]),
]
)
def test_closest_node(
self, n: int, edges: List[List[int]], query: List[List[int]], expected: List[int]
):
actual = closest_node(n, edges, query)
self.assertEqual(expected, actual)
🤖 Prompt for AI Agents
In datastructures/graphs/undirected/test_utils.py around lines 9 to 33, the
test_closest_node signature types expected an int but the test data and
closest_node return a List[int]; update the type hint for expected to be
List[int] to match the function return type and test cases, and optionally
rename the parameter query to queries (and its usages) to reflect it is a list
of query triples.

Comment on lines +11 to +15
def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]:
pass

def bfs_from_node(self, source: Vertex) -> Set[Vertex]:
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

BFS methods currently no-op and violate the Graph contract.

bfs_from_root_to_target and bfs_from_node both just pass, so they return None despite their signatures and the base class docstrings promising a BFS result. That’s a behavioral bug even if these methods aren’t used yet.

I’d recommend at least failing loudly for now:

-    def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]:
-        pass
+    def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]:
+        raise NotImplementedError("bfs_from_root_to_target is not implemented yet")
 
-    def bfs_from_node(self, source: Vertex) -> Set[Vertex]:
-        pass
+    def bfs_from_node(self, source: Vertex) -> Set[Vertex]:
+        raise NotImplementedError("bfs_from_node is not implemented yet")

Or, if you’re ready, implement proper BFS using self.adjacency_list so these methods become usable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]:
pass
def bfs_from_node(self, source: Vertex) -> Set[Vertex]:
pass
def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]:
raise NotImplementedError("bfs_from_root_to_target is not implemented yet")
def bfs_from_node(self, source: Vertex) -> Set[Vertex]:
raise NotImplementedError("bfs_from_node is not implemented yet")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Algorithm Algorithm Problem Datastructures Datastructures Documentation Documentation Updates Graph Graph data structures and algorithms

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants