pizza-lib 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. pizza_lib-0.1.0/LICENSE +21 -0
  2. pizza_lib-0.1.0/PKG-INFO +89 -0
  3. pizza_lib-0.1.0/README.md +64 -0
  4. pizza_lib-0.1.0/pizza_lib.egg-info/PKG-INFO +89 -0
  5. pizza_lib-0.1.0/pizza_lib.egg-info/SOURCES.txt +34 -0
  6. pizza_lib-0.1.0/pizza_lib.egg-info/dependency_links.txt +1 -0
  7. pizza_lib-0.1.0/pizza_lib.egg-info/requires.txt +9 -0
  8. pizza_lib-0.1.0/pizza_lib.egg-info/top_level.txt +1 -0
  9. pizza_lib-0.1.0/pydsa/__init__.py +25 -0
  10. pizza_lib-0.1.0/pydsa/_treeformat.py +224 -0
  11. pizza_lib-0.1.0/pydsa/adj.py +166 -0
  12. pizza_lib-0.1.0/pydsa/arith.py +19 -0
  13. pizza_lib-0.1.0/pydsa/bst.py +78 -0
  14. pizza_lib-0.1.0/pydsa/combo.py +37 -0
  15. pizza_lib-0.1.0/pydsa/counting.py +64 -0
  16. pizza_lib-0.1.0/pydsa/digits.py +50 -0
  17. pizza_lib-0.1.0/pydsa/generators.py +21 -0
  18. pizza_lib-0.1.0/pydsa/graphs.py +441 -0
  19. pizza_lib-0.1.0/pydsa/grid.py +206 -0
  20. pizza_lib-0.1.0/pydsa/heaps.py +128 -0
  21. pizza_lib-0.1.0/pydsa/linked.py +77 -0
  22. pizza_lib-0.1.0/pydsa/nodes.py +72 -0
  23. pizza_lib-0.1.0/pydsa/search.py +248 -0
  24. pizza_lib-0.1.0/pydsa/seq.py +22 -0
  25. pizza_lib-0.1.0/pydsa/tree.py +299 -0
  26. pizza_lib-0.1.0/pyproject.toml +48 -0
  27. pizza_lib-0.1.0/setup.cfg +4 -0
  28. pizza_lib-0.1.0/tests/test_adj.py +119 -0
  29. pizza_lib-0.1.0/tests/test_combo.py +35 -0
  30. pizza_lib-0.1.0/tests/test_counting.py +40 -0
  31. pizza_lib-0.1.0/tests/test_digits.py +74 -0
  32. pizza_lib-0.1.0/tests/test_graphs.py +106 -0
  33. pizza_lib-0.1.0/tests/test_grid.py +183 -0
  34. pizza_lib-0.1.0/tests/test_search.py +63 -0
  35. pizza_lib-0.1.0/tests/test_seq.py +26 -0
  36. pizza_lib-0.1.0/tests/test_tree.py +80 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shyal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: pizza-lib
3
+ Version: 0.1.0
4
+ Summary: Data structure and algorithm helpers for LeetCode-style Python: grids, adjacency, binary search bounds, heaps, trees, linked lists.
5
+ Author: Shyal
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/shyal/pydsa
8
+ Project-URL: Repository, https://github.com/shyal/pydsa
9
+ Keywords: leetcode,algorithms,data-structures,grid,graph,heap,binary-search
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Classifier: Topic :: Education
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: draw
18
+ Requires-Dist: rich; extra == "draw"
19
+ Requires-Dist: colorama; extra == "draw"
20
+ Requires-Dist: PrettyPrintTree; extra == "draw"
21
+ Requires-Dist: graphviz; extra == "draw"
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # pydsa
27
+
28
+ Data structure and algorithm helpers for LeetCode-style Python: the
29
+ functions one keeps rewriting at the top of a solution, written once and
30
+ tested.
31
+
32
+ ```
33
+ pip install pizza-lib # the import name is pydsa
34
+ ```
35
+
36
+ ```python
37
+ from pydsa import table, cells, nbrs, first_true, Multiset, maxheappush
38
+
39
+ dp = table(3, 4) # a 3 by 4 grid of zeros
40
+ for i, j in cells(grid): # every cell, row by row
41
+ for ni, nj in nbrs(grid, i, j): # its on-grid neighbours
42
+ ...
43
+ first_true(1, 10**9, lambda x: x * x >= n) # the first x in [1, 10^9] with ok(x)
44
+ ```
45
+
46
+ ## Modules
47
+
48
+ | Module | Names |
49
+ | --- | --- |
50
+ | `grid` | `table`, `like`, `shape`, `cells`, `nbrs`, `in_bounds`, `is_edge`, `edges`, `put`, `row`, `col`, `set_row`, `set_col`, `grid_bfs`, `CARDINALS`, `DIAGONALS`, `ALL_EIGHT` |
51
+ | `adj` | `adjacency`, `indegrees`, `levels`, `components`, `Graph`, `graph`, `dijkstra` |
52
+ | `search` | `first_true`, `last_true`, `first_false`, `last_false`, `min_chunks`, `viz_binary_search` |
53
+ | `heaps` | `maxheappush`, `maxheappop`, `maxheapify`, `maxheappushpop`, `maxheapreplace`, `maxheappeek`, `maxheapitems`, `heap`, `draw_heap` |
54
+ | `counting` | `Multiset`, `count_pairs` |
55
+ | `combo` | `pairs`, `triples` |
56
+ | `digits` | `to_digits`, `to_int`, `even`, `odd` |
57
+ | `arith` | `ceil_div`, `batched` |
58
+ | `seq` | `lcs` |
59
+ | `generators` | `as_list` |
60
+ | `nodes` | `TreeNode`, `ListNode`, `GraphNode`, `Node` |
61
+ | `tree` | `build_tree`, `get_level_order`, `find_node`, `get_inorder`, `is_balanced`, `build_nary_tree`, `build_general_tree`, `get_general_tree`, `generate_random_tree`, `generate_full_binary_tree`, `draw_tree`, `draw_general_tree` |
62
+ | `bst` | `insert`, `generate_random_bst`, `tree_to_list`, `is_valid_bst`, `generate_and_print_random_bst` |
63
+ | `linked` | `build_linked_list`, `get_list_values`, `print_linked_list`, `linked_list_reverse_k`, `draw_linked_list` |
64
+ | `graphs` | `build_graph`, `get_adj_list`, `build_graph_from_edge_list`, `as_dict_of_dicts`, `is_edge_list`, `draw_graph`, `draw_graphviz`, `draw_ascii_graph` |
65
+
66
+ Every public name is importable from the package itself. The package
67
+ imports only the standard library. The `draw_*` functions, `tabulate`-style
68
+ output and `viz_binary_search` import rich, colorama, PrettyPrintTree and
69
+ graphviz when first called:
70
+
71
+ ```
72
+ pip install 'pizza-lib[draw]'
73
+ ```
74
+
75
+ Every function has a docstring with its contract; the tests in `tests/`
76
+ are the examples.
77
+
78
+ ## Where it comes from
79
+
80
+ These helpers were the harness of the `leet` repo, preloaded into
81
+ `builtins` for every solve there, and the library that the `mu` language
82
+ pastes into the Python it emits. Both still use them from here.
83
+
84
+ ## Tests
85
+
86
+ ```
87
+ pip install -e '.[test]'
88
+ pytest
89
+ ```
@@ -0,0 +1,64 @@
1
+ # pydsa
2
+
3
+ Data structure and algorithm helpers for LeetCode-style Python: the
4
+ functions one keeps rewriting at the top of a solution, written once and
5
+ tested.
6
+
7
+ ```
8
+ pip install pizza-lib # the import name is pydsa
9
+ ```
10
+
11
+ ```python
12
+ from pydsa import table, cells, nbrs, first_true, Multiset, maxheappush
13
+
14
+ dp = table(3, 4) # a 3 by 4 grid of zeros
15
+ for i, j in cells(grid): # every cell, row by row
16
+ for ni, nj in nbrs(grid, i, j): # its on-grid neighbours
17
+ ...
18
+ first_true(1, 10**9, lambda x: x * x >= n) # the first x in [1, 10^9] with ok(x)
19
+ ```
20
+
21
+ ## Modules
22
+
23
+ | Module | Names |
24
+ | --- | --- |
25
+ | `grid` | `table`, `like`, `shape`, `cells`, `nbrs`, `in_bounds`, `is_edge`, `edges`, `put`, `row`, `col`, `set_row`, `set_col`, `grid_bfs`, `CARDINALS`, `DIAGONALS`, `ALL_EIGHT` |
26
+ | `adj` | `adjacency`, `indegrees`, `levels`, `components`, `Graph`, `graph`, `dijkstra` |
27
+ | `search` | `first_true`, `last_true`, `first_false`, `last_false`, `min_chunks`, `viz_binary_search` |
28
+ | `heaps` | `maxheappush`, `maxheappop`, `maxheapify`, `maxheappushpop`, `maxheapreplace`, `maxheappeek`, `maxheapitems`, `heap`, `draw_heap` |
29
+ | `counting` | `Multiset`, `count_pairs` |
30
+ | `combo` | `pairs`, `triples` |
31
+ | `digits` | `to_digits`, `to_int`, `even`, `odd` |
32
+ | `arith` | `ceil_div`, `batched` |
33
+ | `seq` | `lcs` |
34
+ | `generators` | `as_list` |
35
+ | `nodes` | `TreeNode`, `ListNode`, `GraphNode`, `Node` |
36
+ | `tree` | `build_tree`, `get_level_order`, `find_node`, `get_inorder`, `is_balanced`, `build_nary_tree`, `build_general_tree`, `get_general_tree`, `generate_random_tree`, `generate_full_binary_tree`, `draw_tree`, `draw_general_tree` |
37
+ | `bst` | `insert`, `generate_random_bst`, `tree_to_list`, `is_valid_bst`, `generate_and_print_random_bst` |
38
+ | `linked` | `build_linked_list`, `get_list_values`, `print_linked_list`, `linked_list_reverse_k`, `draw_linked_list` |
39
+ | `graphs` | `build_graph`, `get_adj_list`, `build_graph_from_edge_list`, `as_dict_of_dicts`, `is_edge_list`, `draw_graph`, `draw_graphviz`, `draw_ascii_graph` |
40
+
41
+ Every public name is importable from the package itself. The package
42
+ imports only the standard library. The `draw_*` functions, `tabulate`-style
43
+ output and `viz_binary_search` import rich, colorama, PrettyPrintTree and
44
+ graphviz when first called:
45
+
46
+ ```
47
+ pip install 'pizza-lib[draw]'
48
+ ```
49
+
50
+ Every function has a docstring with its contract; the tests in `tests/`
51
+ are the examples.
52
+
53
+ ## Where it comes from
54
+
55
+ These helpers were the harness of the `leet` repo, preloaded into
56
+ `builtins` for every solve there, and the library that the `mu` language
57
+ pastes into the Python it emits. Both still use them from here.
58
+
59
+ ## Tests
60
+
61
+ ```
62
+ pip install -e '.[test]'
63
+ pytest
64
+ ```
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: pizza-lib
3
+ Version: 0.1.0
4
+ Summary: Data structure and algorithm helpers for LeetCode-style Python: grids, adjacency, binary search bounds, heaps, trees, linked lists.
5
+ Author: Shyal
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/shyal/pydsa
8
+ Project-URL: Repository, https://github.com/shyal/pydsa
9
+ Keywords: leetcode,algorithms,data-structures,grid,graph,heap,binary-search
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Classifier: Topic :: Education
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Provides-Extra: draw
18
+ Requires-Dist: rich; extra == "draw"
19
+ Requires-Dist: colorama; extra == "draw"
20
+ Requires-Dist: PrettyPrintTree; extra == "draw"
21
+ Requires-Dist: graphviz; extra == "draw"
22
+ Provides-Extra: test
23
+ Requires-Dist: pytest; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # pydsa
27
+
28
+ Data structure and algorithm helpers for LeetCode-style Python: the
29
+ functions one keeps rewriting at the top of a solution, written once and
30
+ tested.
31
+
32
+ ```
33
+ pip install pizza-lib # the import name is pydsa
34
+ ```
35
+
36
+ ```python
37
+ from pydsa import table, cells, nbrs, first_true, Multiset, maxheappush
38
+
39
+ dp = table(3, 4) # a 3 by 4 grid of zeros
40
+ for i, j in cells(grid): # every cell, row by row
41
+ for ni, nj in nbrs(grid, i, j): # its on-grid neighbours
42
+ ...
43
+ first_true(1, 10**9, lambda x: x * x >= n) # the first x in [1, 10^9] with ok(x)
44
+ ```
45
+
46
+ ## Modules
47
+
48
+ | Module | Names |
49
+ | --- | --- |
50
+ | `grid` | `table`, `like`, `shape`, `cells`, `nbrs`, `in_bounds`, `is_edge`, `edges`, `put`, `row`, `col`, `set_row`, `set_col`, `grid_bfs`, `CARDINALS`, `DIAGONALS`, `ALL_EIGHT` |
51
+ | `adj` | `adjacency`, `indegrees`, `levels`, `components`, `Graph`, `graph`, `dijkstra` |
52
+ | `search` | `first_true`, `last_true`, `first_false`, `last_false`, `min_chunks`, `viz_binary_search` |
53
+ | `heaps` | `maxheappush`, `maxheappop`, `maxheapify`, `maxheappushpop`, `maxheapreplace`, `maxheappeek`, `maxheapitems`, `heap`, `draw_heap` |
54
+ | `counting` | `Multiset`, `count_pairs` |
55
+ | `combo` | `pairs`, `triples` |
56
+ | `digits` | `to_digits`, `to_int`, `even`, `odd` |
57
+ | `arith` | `ceil_div`, `batched` |
58
+ | `seq` | `lcs` |
59
+ | `generators` | `as_list` |
60
+ | `nodes` | `TreeNode`, `ListNode`, `GraphNode`, `Node` |
61
+ | `tree` | `build_tree`, `get_level_order`, `find_node`, `get_inorder`, `is_balanced`, `build_nary_tree`, `build_general_tree`, `get_general_tree`, `generate_random_tree`, `generate_full_binary_tree`, `draw_tree`, `draw_general_tree` |
62
+ | `bst` | `insert`, `generate_random_bst`, `tree_to_list`, `is_valid_bst`, `generate_and_print_random_bst` |
63
+ | `linked` | `build_linked_list`, `get_list_values`, `print_linked_list`, `linked_list_reverse_k`, `draw_linked_list` |
64
+ | `graphs` | `build_graph`, `get_adj_list`, `build_graph_from_edge_list`, `as_dict_of_dicts`, `is_edge_list`, `draw_graph`, `draw_graphviz`, `draw_ascii_graph` |
65
+
66
+ Every public name is importable from the package itself. The package
67
+ imports only the standard library. The `draw_*` functions, `tabulate`-style
68
+ output and `viz_binary_search` import rich, colorama, PrettyPrintTree and
69
+ graphviz when first called:
70
+
71
+ ```
72
+ pip install 'pizza-lib[draw]'
73
+ ```
74
+
75
+ Every function has a docstring with its contract; the tests in `tests/`
76
+ are the examples.
77
+
78
+ ## Where it comes from
79
+
80
+ These helpers were the harness of the `leet` repo, preloaded into
81
+ `builtins` for every solve there, and the library that the `mu` language
82
+ pastes into the Python it emits. Both still use them from here.
83
+
84
+ ## Tests
85
+
86
+ ```
87
+ pip install -e '.[test]'
88
+ pytest
89
+ ```
@@ -0,0 +1,34 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ pizza_lib.egg-info/PKG-INFO
5
+ pizza_lib.egg-info/SOURCES.txt
6
+ pizza_lib.egg-info/dependency_links.txt
7
+ pizza_lib.egg-info/requires.txt
8
+ pizza_lib.egg-info/top_level.txt
9
+ pydsa/__init__.py
10
+ pydsa/_treeformat.py
11
+ pydsa/adj.py
12
+ pydsa/arith.py
13
+ pydsa/bst.py
14
+ pydsa/combo.py
15
+ pydsa/counting.py
16
+ pydsa/digits.py
17
+ pydsa/generators.py
18
+ pydsa/graphs.py
19
+ pydsa/grid.py
20
+ pydsa/heaps.py
21
+ pydsa/linked.py
22
+ pydsa/nodes.py
23
+ pydsa/search.py
24
+ pydsa/seq.py
25
+ pydsa/tree.py
26
+ tests/test_adj.py
27
+ tests/test_combo.py
28
+ tests/test_counting.py
29
+ tests/test_digits.py
30
+ tests/test_graphs.py
31
+ tests/test_grid.py
32
+ tests/test_search.py
33
+ tests/test_seq.py
34
+ tests/test_tree.py
@@ -0,0 +1,9 @@
1
+
2
+ [draw]
3
+ rich
4
+ colorama
5
+ PrettyPrintTree
6
+ graphviz
7
+
8
+ [test]
9
+ pytest
@@ -0,0 +1 @@
1
+ pydsa
@@ -0,0 +1,25 @@
1
+ """pydsa: data structures and algorithm helpers for LeetCode-style Python.
2
+
3
+ Every public name of every module is importable from the package. The
4
+ package imports only the standard library; the drawing functions import
5
+ rich, colorama, PrettyPrintTree and graphviz when first called
6
+ (`pip install pydsa[draw]`).
7
+ """
8
+
9
+ from .adj import * # noqa: F401,F403
10
+ from .arith import * # noqa: F401,F403
11
+ from .bst import * # noqa: F401,F403
12
+ from .combo import * # noqa: F401,F403
13
+ from .counting import * # noqa: F401,F403
14
+ from .digits import * # noqa: F401,F403
15
+ from .generators import * # noqa: F401,F403
16
+ from .graphs import * # noqa: F401,F403
17
+ from .grid import * # noqa: F401,F403
18
+ from .heaps import * # noqa: F401,F403
19
+ from .linked import * # noqa: F401,F403
20
+ from .nodes import * # noqa: F401,F403
21
+ from .search import * # noqa: F401,F403
22
+ from .seq import * # noqa: F401,F403
23
+ from .tree import * # noqa: F401,F403
24
+
25
+ __version__ = "0.1.0"
@@ -0,0 +1,224 @@
1
+ from typing import Any, Callable, Generic, Iterable, TypeVar
2
+
3
+ from PrettyPrint.PrintTree.HorizontalTree import (
4
+ add_parent as add_parent_left,
5
+ )
6
+ from PrettyPrint.PrintTree.HorizontalTree import (
7
+ join_vertically,
8
+ )
9
+ from PrettyPrint.PrintTree.VerticalTree import (
10
+ add_parent as add_parent_top,
11
+ )
12
+ from PrettyPrint.PrintTree.VerticalTree import (
13
+ join_horizontally,
14
+ )
15
+ from PrettyPrint.Utils.NodeFormatter import NodeFormatter
16
+ from PrettyPrint.Utils.Orientation import Orientation
17
+ from PrettyPrint.Utils.StyleAwareUtils import trim_text
18
+
19
+ T = TypeVar("T")
20
+
21
+ # ANSI escape codes for connector foreground colors
22
+ RED = "\033[31m"
23
+ BLUE = "\033[34m"
24
+ RESET = "\033[0m"
25
+
26
+
27
+ class TreeFormatter(Generic[T]):
28
+ def __init__(
29
+ self,
30
+ get_children: Callable[[T], Iterable[T]],
31
+ get_val: Callable[[T], Any],
32
+ get_label: Callable[[T], Any] | None,
33
+ label_color: str,
34
+ show_newline_literal: bool,
35
+ newline_literal: str,
36
+ trim: int,
37
+ trim_symbol: str,
38
+ start_message: Callable[[T], str] | None,
39
+ color: str,
40
+ border: bool,
41
+ max_depth: int,
42
+ orientation: bool,
43
+ ):
44
+ self.get_children = get_children
45
+ self.get_node_val = get_val
46
+ self.get_label = get_label
47
+ self.label_color = label_color
48
+ self.trim = trim
49
+ self.trim_symbol = trim_symbol
50
+ self.show_newline = show_newline_literal
51
+ self.newline_literal = newline_literal
52
+ self.start_message = start_message
53
+ self.color = color
54
+ self.border = border
55
+ self.max_depth = max_depth
56
+ self.orientation = orientation
57
+
58
+ def format(self, node: T) -> str:
59
+ if self.orientation == Orientation.Vertical:
60
+ res = self.tree_vertical_join(node)
61
+ else:
62
+ res = self.tree_horizontal_join(node)
63
+ res = res.to_str().rstrip()
64
+ if self.start_message:
65
+ return f"{ self.start_message(node) }\n{ res }"
66
+ return res
67
+
68
+ def tree_vertical_join(
69
+ self, tree_node: T, depth: int = 0, is_left: bool | None = None
70
+ ) -> NodeFormatter:
71
+ """
72
+ tree_node: actual user tree node (T)
73
+ returns: NodeFormatter for the subtree rooted at tree_node
74
+ is_left: whether tree_node itself is a left child of its parent (used to color the connector to its parent)
75
+ """
76
+ label = self.get_label(tree_node) if self.get_label else None
77
+
78
+ # Get raw children as provided by get_children (may omit None)
79
+ children_raw = list(self.get_children(tree_node))
80
+
81
+ # Create NodeFormatter for current node (styles/colors/trim etc.)
82
+ node = self.add_styles(tree_node)
83
+
84
+ if children_raw and (self.max_depth == -1 or depth < self.max_depth):
85
+ # Build formatted children and remember whether each child is left/right
86
+ children_with_flags = []
87
+ for i, child in enumerate(children_raw):
88
+ # Determine is_left flag correctly for binary trees:
89
+ # - If two children are present, index 0 is left, index 1 is right.
90
+ # - If only one child is present, try to inspect tree_node.left/right attributes.
91
+ if len(children_raw) == 2:
92
+ child_is_left = i == 0
93
+ elif len(children_raw) == 1:
94
+ left_attr = getattr(tree_node, "left", object())
95
+ right_attr = getattr(tree_node, "right", object())
96
+ if child is left_attr:
97
+ child_is_left = True
98
+ elif child is right_attr:
99
+ child_is_left = False
100
+ else:
101
+ # Fallback (best-effort): treat single child as left if we can't detect
102
+ child_is_left = i == 0
103
+ else:
104
+ # No children (shouldn't reach here because children_raw is truthy)
105
+ child_is_left = None
106
+
107
+ child_fmt = self.tree_vertical_join(
108
+ child, depth + 1, is_left=child_is_left
109
+ )
110
+ children_with_flags.append((child_fmt, child_is_left))
111
+
112
+ if len(children_with_flags) == 1:
113
+ # Single-child case: insert a colored '|' connector above the child
114
+ child_node_fmt, child_is_left = children_with_flags[0]
115
+ if child_is_left is True:
116
+ edge_str = f"{RED}/{RESET}"
117
+ elif child_is_left is False:
118
+ edge_str = f"{BLUE}\\{RESET}"
119
+ else:
120
+ edge_str = "|"
121
+ child_node_fmt.lines.insert(
122
+ 0, " " * child_node_fmt.get_middle_width() + edge_str
123
+ )
124
+ children_node = child_node_fmt
125
+ else:
126
+ # Two children: join them horizontally (we rely on join_horizontally for placement)
127
+ children_node = join_horizontally(
128
+ [fmt for fmt, _ in children_with_flags]
129
+ )
130
+
131
+ node = add_parent_top(node, children_node)
132
+
133
+ node = self.add_label(label, node, add_parent_top, "|")
134
+ return node
135
+
136
+ def tree_horizontal_join(
137
+ self, tree_node: T, depth: int = 0, is_left: bool | None = None
138
+ ) -> NodeFormatter:
139
+ """
140
+ Horizontal join variant (similar logic to vertical).
141
+ """
142
+ label = self.get_label(tree_node) if self.get_label else None
143
+ children_raw = list(self.get_children(tree_node))
144
+ node = self.add_styles(tree_node)
145
+ node_padding = " " * node.width
146
+ node.lines = node.lines + [node_padding]
147
+ node.height += 1
148
+
149
+ if children_raw and (self.max_depth == -1 or depth < self.max_depth):
150
+ children_with_flags = []
151
+ for i, child in enumerate(children_raw):
152
+ if len(children_raw) == 2:
153
+ child_is_left = i == 0
154
+ elif len(children_raw) == 1:
155
+ left_attr = getattr(tree_node, "left", object())
156
+ right_attr = getattr(tree_node, "right", object())
157
+ if child is left_attr:
158
+ child_is_left = True
159
+ elif child is right_attr:
160
+ child_is_left = False
161
+ else:
162
+ child_is_left = i == 0
163
+ else:
164
+ child_is_left = None
165
+
166
+ child_fmt = self.tree_horizontal_join(
167
+ child, depth + 1, is_left=child_is_left
168
+ )
169
+ children_with_flags.append((child_fmt, child_is_left))
170
+
171
+ if len(children_with_flags) == 1:
172
+ children_node, child_is_left = children_with_flags[0]
173
+ middle = children_node.get_middle_height()
174
+ new_lines = []
175
+ for r_idx, line in enumerate(children_node.lines):
176
+ if r_idx == middle:
177
+ if child_is_left is True:
178
+ edge_str = f"{RED}─{RESET}"
179
+ elif child_is_left is False:
180
+ edge_str = f"{BLUE}─{RESET}"
181
+ else:
182
+ edge_str = "─"
183
+ new_lines.append(edge_str + line)
184
+ else:
185
+ new_lines.append(" " + line)
186
+ children_node.lines = new_lines
187
+ else:
188
+ children_node = join_vertically([fmt for fmt, _ in children_with_flags])
189
+
190
+ node = add_parent_left(node, children_node)
191
+
192
+ node = self.add_label(label, node, add_parent_left, "─")
193
+ return node
194
+
195
+ def add_label(
196
+ self,
197
+ label: Any,
198
+ node: NodeFormatter,
199
+ parent_adder: Callable[[NodeFormatter, NodeFormatter], NodeFormatter],
200
+ seperator: str,
201
+ ) -> NodeFormatter:
202
+ if label:
203
+ label = NodeFormatter.from_string(str(label))
204
+ if self.label_color:
205
+ label.color_bg(self.label_color, True)
206
+ node = parent_adder(NodeFormatter.from_string(seperator), node)
207
+ node = parent_adder(label, node)
208
+ return node
209
+
210
+ def add_styles(self, tree_node: T) -> NodeFormatter:
211
+ """
212
+ Build and return a NodeFormatter for the given tree node (apply trim, newline literal, background color, border).
213
+ """
214
+ contents = str(self.get_node_val(tree_node))
215
+ if self.show_newline:
216
+ contents = contents.replace("\n", self.newline_literal)
217
+ if self.trim != -1:
218
+ contents = trim_text(contents, self.trim, self.trim_symbol)
219
+ node = NodeFormatter.from_string(contents)
220
+ if self.border:
221
+ node.add_border()
222
+ if self.color:
223
+ node.color_bg(self.color, add_space=not self.border)
224
+ return node