pytrilogy 0.0.2.58__py3-none-any.whl → 0.0.3.0__py3-none-any.whl
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.
- {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/METADATA +9 -2
- pytrilogy-0.0.3.0.dist-info/RECORD +99 -0
- {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/WHEEL +1 -1
- trilogy/__init__.py +2 -2
- trilogy/core/enums.py +1 -7
- trilogy/core/env_processor.py +17 -5
- trilogy/core/environment_helpers.py +11 -25
- trilogy/core/exceptions.py +4 -0
- trilogy/core/functions.py +695 -261
- trilogy/core/graph_models.py +10 -10
- trilogy/core/internal.py +11 -2
- trilogy/core/models/__init__.py +0 -0
- trilogy/core/models/author.py +2110 -0
- trilogy/core/models/build.py +1845 -0
- trilogy/core/models/build_environment.py +151 -0
- trilogy/core/models/core.py +370 -0
- trilogy/core/models/datasource.py +297 -0
- trilogy/core/models/environment.py +696 -0
- trilogy/core/models/execute.py +931 -0
- trilogy/core/optimization.py +14 -16
- trilogy/core/optimizations/base_optimization.py +1 -1
- trilogy/core/optimizations/inline_constant.py +6 -6
- trilogy/core/optimizations/inline_datasource.py +17 -11
- trilogy/core/optimizations/predicate_pushdown.py +17 -16
- trilogy/core/processing/concept_strategies_v3.py +180 -145
- trilogy/core/processing/graph_utils.py +1 -1
- trilogy/core/processing/node_generators/basic_node.py +19 -18
- trilogy/core/processing/node_generators/common.py +50 -44
- trilogy/core/processing/node_generators/filter_node.py +26 -13
- trilogy/core/processing/node_generators/group_node.py +26 -21
- trilogy/core/processing/node_generators/group_to_node.py +11 -8
- trilogy/core/processing/node_generators/multiselect_node.py +60 -43
- trilogy/core/processing/node_generators/node_merge_node.py +76 -38
- trilogy/core/processing/node_generators/rowset_node.py +57 -36
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +27 -34
- trilogy/core/processing/node_generators/select_merge_node.py +161 -64
- trilogy/core/processing/node_generators/select_node.py +13 -13
- trilogy/core/processing/node_generators/union_node.py +12 -11
- trilogy/core/processing/node_generators/unnest_node.py +9 -7
- trilogy/core/processing/node_generators/window_node.py +19 -16
- trilogy/core/processing/nodes/__init__.py +21 -18
- trilogy/core/processing/nodes/base_node.py +82 -66
- trilogy/core/processing/nodes/filter_node.py +19 -13
- trilogy/core/processing/nodes/group_node.py +50 -35
- trilogy/core/processing/nodes/merge_node.py +45 -36
- trilogy/core/processing/nodes/select_node_v2.py +53 -39
- trilogy/core/processing/nodes/union_node.py +5 -7
- trilogy/core/processing/nodes/unnest_node.py +7 -11
- trilogy/core/processing/nodes/window_node.py +9 -4
- trilogy/core/processing/utility.py +103 -75
- trilogy/core/query_processor.py +65 -47
- trilogy/core/statements/__init__.py +0 -0
- trilogy/core/statements/author.py +413 -0
- trilogy/core/statements/build.py +0 -0
- trilogy/core/statements/common.py +30 -0
- trilogy/core/statements/execute.py +42 -0
- trilogy/dialect/base.py +146 -106
- trilogy/dialect/common.py +9 -10
- trilogy/dialect/duckdb.py +1 -1
- trilogy/dialect/enums.py +4 -2
- trilogy/dialect/presto.py +1 -1
- trilogy/dialect/sql_server.py +1 -1
- trilogy/executor.py +44 -32
- trilogy/hooks/base_hook.py +6 -4
- trilogy/hooks/query_debugger.py +110 -93
- trilogy/parser.py +1 -1
- trilogy/parsing/common.py +303 -64
- trilogy/parsing/parse_engine.py +263 -617
- trilogy/parsing/render.py +50 -26
- trilogy/scripts/trilogy.py +2 -1
- pytrilogy-0.0.2.58.dist-info/RECORD +0 -87
- trilogy/core/models.py +0 -4960
- {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.58.dist-info → pytrilogy-0.0.3.0.dist-info}/top_level.txt +0 -0
trilogy/executor.py
CHANGED
|
@@ -7,29 +7,29 @@ from sqlalchemy import text
|
|
|
7
7
|
from sqlalchemy.engine import CursorResult, Engine
|
|
8
8
|
|
|
9
9
|
from trilogy.constants import logger
|
|
10
|
-
from trilogy.core.enums import Granularity, IOType
|
|
11
|
-
from trilogy.core.models import
|
|
12
|
-
|
|
10
|
+
from trilogy.core.enums import FunctionType, Granularity, IOType
|
|
11
|
+
from trilogy.core.models.author import Concept, Function
|
|
12
|
+
from trilogy.core.models.build import BuildConcept, BuildFunction
|
|
13
|
+
from trilogy.core.models.core import ListWrapper, MapWrapper
|
|
14
|
+
from trilogy.core.models.datasource import Datasource
|
|
15
|
+
from trilogy.core.models.environment import Environment
|
|
16
|
+
from trilogy.core.statements.author import (
|
|
13
17
|
ConceptDeclarationStatement,
|
|
14
18
|
CopyStatement,
|
|
15
|
-
Datasource,
|
|
16
|
-
Environment,
|
|
17
|
-
Function,
|
|
18
|
-
FunctionType,
|
|
19
19
|
ImportStatement,
|
|
20
|
-
ListWrapper,
|
|
21
|
-
MapWrapper,
|
|
22
20
|
MergeStatementV2,
|
|
23
21
|
MultiSelectStatement,
|
|
24
22
|
PersistStatement,
|
|
23
|
+
RawSQLStatement,
|
|
24
|
+
SelectStatement,
|
|
25
|
+
ShowStatement,
|
|
26
|
+
)
|
|
27
|
+
from trilogy.core.statements.execute import (
|
|
25
28
|
ProcessedCopyStatement,
|
|
26
29
|
ProcessedQuery,
|
|
27
30
|
ProcessedQueryPersist,
|
|
28
31
|
ProcessedRawSQLStatement,
|
|
29
32
|
ProcessedShowStatement,
|
|
30
|
-
RawSQLStatement,
|
|
31
|
-
SelectStatement,
|
|
32
|
-
ShowStatement,
|
|
33
33
|
)
|
|
34
34
|
from trilogy.dialect.base import BaseDialect
|
|
35
35
|
from trilogy.dialect.enums import Dialects
|
|
@@ -58,7 +58,9 @@ class MockResult:
|
|
|
58
58
|
return self.columns
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def generate_result_set(
|
|
61
|
+
def generate_result_set(
|
|
62
|
+
columns: List[BuildConcept], output_data: list[Any]
|
|
63
|
+
) -> MockResult:
|
|
62
64
|
names = [x.address.replace(".", "_") for x in columns]
|
|
63
65
|
return MockResult(
|
|
64
66
|
values=[dict(zip(names, [row])) for row in output_data], columns=names
|
|
@@ -394,6 +396,32 @@ class Executor(object):
|
|
|
394
396
|
if persist and isinstance(x, ProcessedQueryPersist):
|
|
395
397
|
self.environment.add_datasource(x.datasource)
|
|
396
398
|
|
|
399
|
+
def _concept_to_value(
|
|
400
|
+
self,
|
|
401
|
+
concept: Concept,
|
|
402
|
+
local_concepts: dict[str, Concept] | None = None,
|
|
403
|
+
) -> Any:
|
|
404
|
+
if not concept.granularity == Granularity.SINGLE_ROW:
|
|
405
|
+
raise SyntaxError(f"Cannot bind non-singleton concept {concept.address}")
|
|
406
|
+
# TODO: to get rid of function here - need to figure out why it's getting passed in
|
|
407
|
+
if (
|
|
408
|
+
isinstance(concept.lineage, (BuildFunction, Function))
|
|
409
|
+
and concept.lineage.operator == FunctionType.CONSTANT
|
|
410
|
+
):
|
|
411
|
+
rval = concept.lineage.arguments[0]
|
|
412
|
+
if isinstance(rval, ListWrapper):
|
|
413
|
+
return [x for x in rval]
|
|
414
|
+
if isinstance(rval, MapWrapper):
|
|
415
|
+
return {k: v for k, v in rval.items()}
|
|
416
|
+
# if isinstance(rval, ConceptRef):
|
|
417
|
+
# return self._concept_to_value(self.environment.concepts[rval.address], local_concepts=local_concepts)
|
|
418
|
+
return rval
|
|
419
|
+
else:
|
|
420
|
+
results = self.execute_query(f"select {concept.name} limit 1;").fetchone()
|
|
421
|
+
if not results:
|
|
422
|
+
return None
|
|
423
|
+
return results[0]
|
|
424
|
+
|
|
397
425
|
def _hydrate_param(
|
|
398
426
|
self, param: str, local_concepts: dict[str, Concept] | None = None
|
|
399
427
|
) -> Any:
|
|
@@ -412,23 +440,7 @@ class Executor(object):
|
|
|
412
440
|
raise SyntaxError(f"No concept found for parameter {param}")
|
|
413
441
|
|
|
414
442
|
concept: Concept = matched.pop()
|
|
415
|
-
|
|
416
|
-
raise SyntaxError(f"Cannot bind non-singleton concept {concept.address}")
|
|
417
|
-
if (
|
|
418
|
-
isinstance(concept.lineage, Function)
|
|
419
|
-
and concept.lineage.operator == FunctionType.CONSTANT
|
|
420
|
-
):
|
|
421
|
-
rval = concept.lineage.arguments[0]
|
|
422
|
-
if isinstance(rval, ListWrapper):
|
|
423
|
-
return [x for x in rval]
|
|
424
|
-
if isinstance(rval, MapWrapper):
|
|
425
|
-
return {k: v for k, v in rval.items()}
|
|
426
|
-
return rval
|
|
427
|
-
else:
|
|
428
|
-
results = self.execute_query(f"select {concept.name} limit 1;").fetchone()
|
|
429
|
-
if not results:
|
|
430
|
-
return None
|
|
431
|
-
return results[0]
|
|
443
|
+
return self._concept_to_value(concept, local_concepts=local_concepts)
|
|
432
444
|
|
|
433
445
|
def execute_raw_sql(
|
|
434
446
|
self,
|
|
@@ -437,7 +449,7 @@ class Executor(object):
|
|
|
437
449
|
local_concepts: dict[str, Concept] | None = None,
|
|
438
450
|
) -> CursorResult:
|
|
439
451
|
"""Run a command against the raw underlying
|
|
440
|
-
execution engine"""
|
|
452
|
+
execution engine."""
|
|
441
453
|
final_params = None
|
|
442
454
|
q = text(command)
|
|
443
455
|
if variables:
|
|
@@ -459,7 +471,7 @@ class Executor(object):
|
|
|
459
471
|
def execute_text(
|
|
460
472
|
self, command: str, non_interactive: bool = False
|
|
461
473
|
) -> List[CursorResult]:
|
|
462
|
-
"""Run a
|
|
474
|
+
"""Run a trilogy query expressed as text."""
|
|
463
475
|
output = []
|
|
464
476
|
# connection = self.engine.connect()
|
|
465
477
|
for statement in self.parse_text_generator(command):
|
trilogy/hooks/base_hook.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
from trilogy.core.models import (
|
|
1
|
+
from trilogy.core.models.execute import (
|
|
2
2
|
CTE,
|
|
3
|
+
QueryDatasource,
|
|
4
|
+
UnionCTE,
|
|
5
|
+
)
|
|
6
|
+
from trilogy.core.processing.nodes import StrategyNode
|
|
7
|
+
from trilogy.core.statements.author import (
|
|
3
8
|
MultiSelectStatement,
|
|
4
9
|
PersistStatement,
|
|
5
|
-
QueryDatasource,
|
|
6
10
|
RowsetDerivationStatement,
|
|
7
11
|
SelectStatement,
|
|
8
|
-
UnionCTE,
|
|
9
12
|
)
|
|
10
|
-
from trilogy.core.processing.nodes import StrategyNode
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class BaseHook:
|
trilogy/hooks/query_debugger.py
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from logging import DEBUG, StreamHandler
|
|
3
3
|
from typing import Union
|
|
4
|
+
from uuid import uuid4
|
|
4
5
|
|
|
5
6
|
from trilogy.constants import logger
|
|
6
|
-
from trilogy.core.models import
|
|
7
|
+
from trilogy.core.models.build import BuildDatasource
|
|
8
|
+
from trilogy.core.models.execute import (
|
|
7
9
|
CTE,
|
|
8
|
-
Datasource,
|
|
9
10
|
QueryDatasource,
|
|
10
|
-
SelectStatement,
|
|
11
11
|
UnionCTE,
|
|
12
12
|
)
|
|
13
13
|
from trilogy.core.processing.nodes import StrategyNode
|
|
14
|
+
from trilogy.core.statements.author import SelectStatement
|
|
14
15
|
from trilogy.dialect.bigquery import BigqueryDialect
|
|
15
16
|
from trilogy.hooks.base_hook import BaseHook
|
|
16
17
|
|
|
@@ -24,90 +25,6 @@ class PrintMode(Enum):
|
|
|
24
25
|
renderer = BigqueryDialect()
|
|
25
26
|
|
|
26
27
|
|
|
27
|
-
def print_recursive_resolved(
|
|
28
|
-
input: Union[QueryDatasource, Datasource], mode: PrintMode, depth: int = 0
|
|
29
|
-
):
|
|
30
|
-
extra = []
|
|
31
|
-
if isinstance(input, QueryDatasource):
|
|
32
|
-
if input.joins:
|
|
33
|
-
extra.append("join")
|
|
34
|
-
if input.condition:
|
|
35
|
-
extra.append("filter")
|
|
36
|
-
if input.group_required:
|
|
37
|
-
extra.append("group")
|
|
38
|
-
output = [c.address for c in input.output_concepts[:3]]
|
|
39
|
-
if len(input.output_concepts) > 3:
|
|
40
|
-
output.append("...")
|
|
41
|
-
display = [
|
|
42
|
-
(
|
|
43
|
-
" " * depth,
|
|
44
|
-
input.__class__.__name__,
|
|
45
|
-
"<",
|
|
46
|
-
",".join(extra),
|
|
47
|
-
">",
|
|
48
|
-
# [c.address for c in input.input_concepts],
|
|
49
|
-
"->",
|
|
50
|
-
output,
|
|
51
|
-
)
|
|
52
|
-
]
|
|
53
|
-
if isinstance(input, QueryDatasource):
|
|
54
|
-
for child in input.datasources:
|
|
55
|
-
display += print_recursive_resolved(child, mode=mode, depth=depth + 1)
|
|
56
|
-
return display
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def print_recursive_nodes(
|
|
60
|
-
input: StrategyNode, mode: PrintMode = PrintMode.BASIC, depth: int = 0
|
|
61
|
-
):
|
|
62
|
-
resolved = input.resolve()
|
|
63
|
-
if mode == PrintMode.FULL:
|
|
64
|
-
display = [
|
|
65
|
-
[
|
|
66
|
-
" " * depth,
|
|
67
|
-
input,
|
|
68
|
-
"->",
|
|
69
|
-
resolved.grain,
|
|
70
|
-
"->",
|
|
71
|
-
[c.address for c in resolved.output_concepts],
|
|
72
|
-
]
|
|
73
|
-
]
|
|
74
|
-
elif mode == PrintMode.BASIC:
|
|
75
|
-
display = [
|
|
76
|
-
[
|
|
77
|
-
" " * depth,
|
|
78
|
-
input,
|
|
79
|
-
"->",
|
|
80
|
-
resolved.grain,
|
|
81
|
-
]
|
|
82
|
-
]
|
|
83
|
-
for child in input.parents:
|
|
84
|
-
display += print_recursive_nodes(
|
|
85
|
-
child,
|
|
86
|
-
mode=mode,
|
|
87
|
-
depth=depth + 1,
|
|
88
|
-
)
|
|
89
|
-
return display
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def print_recursive_ctes(
|
|
93
|
-
input: CTE | UnionCTE, depth: int = 0, max_depth: int | None = None
|
|
94
|
-
):
|
|
95
|
-
if max_depth and depth > max_depth:
|
|
96
|
-
return
|
|
97
|
-
select_statement = [c.address for c in input.output_columns]
|
|
98
|
-
print(" " * depth, input.name, "->", input.group_to_grain, "->", select_statement)
|
|
99
|
-
sql = renderer.render_cte(input).statement
|
|
100
|
-
for line in sql.split("\n"):
|
|
101
|
-
logger.debug(" " * (depth) + line)
|
|
102
|
-
if isinstance(input, CTE):
|
|
103
|
-
for child in input.parent_ctes:
|
|
104
|
-
print_recursive_ctes(child, depth + 1)
|
|
105
|
-
elif isinstance(input, UnionCTE):
|
|
106
|
-
for child in input.parent_ctes:
|
|
107
|
-
for parent in child.parent_ctes:
|
|
108
|
-
print_recursive_ctes(parent, depth + 1)
|
|
109
|
-
|
|
110
|
-
|
|
111
28
|
class DebuggingHook(BaseHook):
|
|
112
29
|
def __init__(
|
|
113
30
|
self,
|
|
@@ -127,24 +44,124 @@ class DebuggingHook(BaseHook):
|
|
|
127
44
|
self.process_nodes = PrintMode(process_nodes)
|
|
128
45
|
self.process_datasources = PrintMode(process_datasources)
|
|
129
46
|
self.process_other = PrintMode(process_other)
|
|
47
|
+
self.messages: list[str] = []
|
|
48
|
+
self.uuid = uuid4()
|
|
49
|
+
|
|
50
|
+
def print(self, *args):
|
|
51
|
+
merged = " ".join([str(x) for x in args])
|
|
52
|
+
self.messages.append(merged)
|
|
53
|
+
|
|
54
|
+
def write(self):
|
|
55
|
+
with open(f"debug_{self.uuid}.log", "w") as f:
|
|
56
|
+
f.write("\n".join(self.messages))
|
|
130
57
|
|
|
131
58
|
def process_select_info(self, select: SelectStatement):
|
|
132
59
|
if self.process_datasources != PrintMode.OFF:
|
|
133
|
-
print(f"grain: {str(select.grain)}")
|
|
60
|
+
self.print(f"grain: {str(select.grain)}")
|
|
134
61
|
|
|
135
62
|
def process_root_datasource(self, datasource: QueryDatasource):
|
|
136
63
|
if self.process_datasources != PrintMode.OFF:
|
|
137
|
-
printed = print_recursive_resolved(
|
|
64
|
+
printed = self.print_recursive_resolved(
|
|
65
|
+
datasource, self.process_datasources
|
|
66
|
+
)
|
|
138
67
|
for row in printed:
|
|
139
|
-
print("".join([str(v) for v in row]))
|
|
68
|
+
self.print("".join([str(v) for v in row]))
|
|
140
69
|
|
|
141
70
|
def process_root_cte(self, cte: CTE | UnionCTE):
|
|
142
71
|
if self.process_ctes != PrintMode.OFF:
|
|
143
|
-
print_recursive_ctes(cte, max_depth=self.max_depth)
|
|
72
|
+
self.print_recursive_ctes(cte, max_depth=self.max_depth)
|
|
144
73
|
|
|
145
74
|
def process_root_strategy_node(self, node: StrategyNode):
|
|
146
75
|
if self.process_nodes != PrintMode.OFF:
|
|
147
|
-
printed = print_recursive_nodes(node, mode=self.process_nodes)
|
|
76
|
+
printed = self.print_recursive_nodes(node, mode=self.process_nodes)
|
|
148
77
|
for row in printed:
|
|
149
78
|
# logger.info("".join([str(v) for v in row]))
|
|
150
|
-
print("".join([str(v) for v in row]))
|
|
79
|
+
self.print("".join([str(v) for v in row]))
|
|
80
|
+
|
|
81
|
+
def print_recursive_resolved(
|
|
82
|
+
self,
|
|
83
|
+
input: Union[QueryDatasource, BuildDatasource],
|
|
84
|
+
mode: PrintMode,
|
|
85
|
+
depth: int = 0,
|
|
86
|
+
):
|
|
87
|
+
extra = []
|
|
88
|
+
if isinstance(input, QueryDatasource):
|
|
89
|
+
if input.joins:
|
|
90
|
+
extra.append("join")
|
|
91
|
+
if input.condition:
|
|
92
|
+
extra.append("filter")
|
|
93
|
+
if input.group_required:
|
|
94
|
+
extra.append("group")
|
|
95
|
+
output = [c.address for c in input.output_concepts[:3]]
|
|
96
|
+
if len(input.output_concepts) > 3:
|
|
97
|
+
output.append("...")
|
|
98
|
+
display = [
|
|
99
|
+
(
|
|
100
|
+
" " * depth,
|
|
101
|
+
input.__class__.__name__,
|
|
102
|
+
"<",
|
|
103
|
+
",".join(extra),
|
|
104
|
+
">",
|
|
105
|
+
# [c.address for c in input.input_concepts],
|
|
106
|
+
"->",
|
|
107
|
+
output,
|
|
108
|
+
)
|
|
109
|
+
]
|
|
110
|
+
if isinstance(input, QueryDatasource):
|
|
111
|
+
for child in input.datasources:
|
|
112
|
+
display += self.print_recursive_resolved(
|
|
113
|
+
child, mode=mode, depth=depth + 1
|
|
114
|
+
)
|
|
115
|
+
return display
|
|
116
|
+
|
|
117
|
+
def print_recursive_ctes(
|
|
118
|
+
self, input: CTE | UnionCTE, depth: int = 0, max_depth: int | None = None
|
|
119
|
+
):
|
|
120
|
+
if max_depth and depth > max_depth:
|
|
121
|
+
return
|
|
122
|
+
select_statement = [c.address for c in input.output_columns]
|
|
123
|
+
self.print(
|
|
124
|
+
" " * depth, input.name, "->", input.group_to_grain, "->", select_statement
|
|
125
|
+
)
|
|
126
|
+
sql = renderer.render_cte(input).statement
|
|
127
|
+
for line in sql.split("\n"):
|
|
128
|
+
logger.debug(" " * (depth) + line)
|
|
129
|
+
if isinstance(input, CTE):
|
|
130
|
+
for child in input.parent_ctes:
|
|
131
|
+
self.print_recursive_ctes(child, depth + 1)
|
|
132
|
+
elif isinstance(input, UnionCTE):
|
|
133
|
+
for child in input.parent_ctes:
|
|
134
|
+
for parent in child.parent_ctes:
|
|
135
|
+
self.print_recursive_ctes(parent, depth + 1)
|
|
136
|
+
|
|
137
|
+
def print_recursive_nodes(
|
|
138
|
+
self, input: StrategyNode, mode: PrintMode = PrintMode.BASIC, depth: int = 0
|
|
139
|
+
):
|
|
140
|
+
resolved = input.resolve()
|
|
141
|
+
if mode == PrintMode.FULL:
|
|
142
|
+
display = [
|
|
143
|
+
[
|
|
144
|
+
" " * depth,
|
|
145
|
+
input,
|
|
146
|
+
"->",
|
|
147
|
+
resolved.grain,
|
|
148
|
+
"->",
|
|
149
|
+
[c.address for c in resolved.output_concepts],
|
|
150
|
+
]
|
|
151
|
+
]
|
|
152
|
+
elif mode == PrintMode.BASIC:
|
|
153
|
+
display = [
|
|
154
|
+
[
|
|
155
|
+
" " * depth,
|
|
156
|
+
input,
|
|
157
|
+
"->",
|
|
158
|
+
resolved.grain,
|
|
159
|
+
]
|
|
160
|
+
]
|
|
161
|
+
for child in input.parents:
|
|
162
|
+
display += self.print_recursive_nodes(
|
|
163
|
+
child,
|
|
164
|
+
mode=mode,
|
|
165
|
+
depth=depth + 1,
|
|
166
|
+
)
|
|
167
|
+
return display
|