Skip to content

Commit c29e8fb

Browse files
committed
refactor: render spec line recursively on rust side
1 parent f77961d commit c29e8fb

File tree

3 files changed

+102
-91
lines changed

3 files changed

+102
-91
lines changed

python/cocoindex/flow.py

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -458,38 +458,19 @@ def _render_spec(self, verbose: bool = False) -> Tree:
458458
"""
459459
Render the flow spec as a styled rich Tree with hierarchical structure.
460460
"""
461+
spec = self._get_spec(verbose=verbose)
461462
tree = Tree(f"Flow: {self.name}", style="cyan")
462-
current_section = None
463-
section_node = None
464-
indent_stack = []
465-
466-
for i, (section, content, indent) in enumerate(self._get_spec(verbose=verbose)):
467-
# Skip "Scope" entries (see ReactiveOpScope in spec.rs)
468-
if content.startswith("Scope:"):
469-
continue
470-
471-
if section != current_section:
472-
current_section = section
473-
section_node = tree.add(f"{section}:", style="bold magenta")
474-
indent_stack = [(0, section_node)]
475-
476-
while indent_stack and indent_stack[-1][0] >= indent:
477-
indent_stack.pop()
478-
479-
parent = indent_stack[-1][1] if indent_stack else section_node
480-
styled_content = Text(content, style="yellow")
481-
is_parent = any(
482-
next_indent > indent
483-
for _, next_content, next_indent in self._get_spec(verbose=verbose)[i + 1:]
484-
if not next_content.startswith("Scope:")
485-
)
486463

487-
if is_parent:
488-
node = parent.add(styled_content, style=None)
489-
indent_stack.append((indent, node))
490-
else:
491-
parent.add(styled_content, style=None)
464+
def build_tree(label: str, lines: list):
465+
node = Tree(label, style="bold magenta" if lines else "cyan")
466+
for line in lines:
467+
child_node = node.add(Text(line.content, style="yellow"))
468+
child_node.children = build_tree("", line.children).children
469+
return node
492470

471+
for section, lines in spec.sections:
472+
section_node = build_tree(f"{section}:", lines)
473+
tree.children.append(section_node)
493474
return tree
494475

495476
def _get_spec(self, verbose: bool = False) -> list[tuple[str, str, int]]:

src/base/spec.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,45 @@
11
use crate::prelude::*;
22

33
use super::schema::{EnrichedValueType, FieldSchema};
4+
use pyo3::prelude::*;
45
use serde::{Deserialize, Serialize};
56
use std::fmt;
67
use std::ops::Deref;
78

89
/// OutputMode enum for displaying spec info in different granularity
9-
#[derive(Debug, Clone, Copy, PartialEq)]
10+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
11+
#[serde(rename_all = "lowercase")]
1012
pub enum OutputMode {
1113
Concise,
1214
Verbose,
1315
}
1416

15-
impl OutputMode {
16-
pub fn from_str(s: &str) -> Self {
17-
match s.to_lowercase().as_str() {
18-
"concise" => OutputMode::Concise,
19-
"verbose" => OutputMode::Verbose,
20-
_ => unreachable!(
21-
"Invalid format mode: {}. Expected 'concise' or 'verbose'.",
22-
s
23-
),
24-
}
25-
}
26-
}
27-
2817
/// Formatting spec per output mode
2918
pub trait SpecFormatter {
3019
fn format(&self, mode: OutputMode) -> String;
3120
}
3221

22+
/// A single line in the rendered spec, with optional scope and children
23+
#[pyclass(get_all, set_all)]
24+
#[derive(Debug, Clone, Serialize, Deserialize)]
25+
pub struct RenderedSpecLine {
26+
/// The formatted content of the line (e.g., "Import: name=documents, source=LocalFile")
27+
pub content: String,
28+
/// The scope name, if applicable (e.g., "documents_1" for ForEach scopes)
29+
#[serde(default, skip_serializing_if = "Option::is_none")]
30+
pub scope: Option<String>,
31+
/// Child lines in the hierarchy
32+
pub children: Vec<RenderedSpecLine>,
33+
}
34+
35+
/// A rendered specification, grouped by sections
36+
#[pyclass(get_all, set_all)]
37+
#[derive(Debug, Clone, Serialize, Deserialize)]
38+
pub struct RenderedSpec {
39+
/// List of (section_name, lines) pairs
40+
pub sections: Vec<(String, Vec<RenderedSpecLine>)>,
41+
}
42+
3343
#[derive(Debug, Clone, Serialize, Deserialize)]
3444
#[serde(tag = "kind")]
3545
pub enum SpecString {
@@ -190,7 +200,7 @@ impl ValueMapping {
190200
}
191201

192202
impl std::fmt::Display for ValueMapping {
193-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
194204
match self {
195205
ValueMapping::Constant(v) => write!(
196206
f,

src/py/mod.rs

Lines changed: 67 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::prelude::*;
33
use crate::base::schema::{FieldSchema, ValueType};
44
use crate::base::spec::VectorSimilarityMetric;
55
use crate::base::spec::{NamedSpec, ReactiveOpSpec};
6-
use crate::base::spec::{OutputMode, SpecFormatter};
6+
use crate::base::spec::{OutputMode, RenderedSpec, RenderedSpecLine, SpecFormatter};
77
use crate::execution::query;
88
use crate::lib_context::{clear_lib_context, get_auth_registry, init_lib_context};
99
use crate::ops::interface::{QueryResult, QueryResults};
@@ -198,76 +198,94 @@ impl Flow {
198198
})
199199
}
200200

201-
#[pyo3(signature = (output_mode="concise"))]
202-
pub fn get_spec(&self, output_mode: &str) -> PyResult<Vec<(String, String, u32)>> {
203-
let mode = OutputMode::from_str(output_mode);
201+
#[pyo3(signature = (output_mode=None))]
202+
pub fn get_spec(&self, output_mode: Option<Pythonized<OutputMode>>) -> PyResult<RenderedSpec> {
203+
let mode = output_mode.map_or(OutputMode::Concise, |m| m.into_inner());
204204
let spec = &self.0.flow.flow_instance;
205-
let mut result = Vec::with_capacity(
206-
1 + spec.import_ops.len()
207-
+ spec.reactive_ops.len()
208-
+ spec.export_ops.len()
209-
+ spec.declarations.len(),
205+
let mut sections: IndexMap<String, Vec<RenderedSpecLine>> = IndexMap::new();
206+
207+
// Initialize sections
208+
sections.insert(
209+
"Header".to_string(),
210+
vec![RenderedSpecLine {
211+
content: format!("Flow: {}", spec.name),
212+
scope: None,
213+
children: vec![],
214+
}],
210215
);
211-
212-
fn extend_with<T, I, F>(
213-
out: &mut Vec<(String, String, u32)>,
214-
label: &'static str,
215-
items: I,
216-
f: F,
217-
) where
218-
I: IntoIterator<Item = T>,
219-
F: Fn(&T) -> String,
220-
{
221-
out.extend(items.into_iter().map(|item| (label.into(), f(&item), 0)));
216+
for key in ["Sources", "Processing", "Targets", "Declarations"] {
217+
sections.insert(key.to_string(), Vec::new());
222218
}
223219

224-
// Header
225-
result.push(("Header".into(), format!("Flow: {}", spec.name), 0));
226-
227220
// Sources
228-
extend_with(&mut result, "Sources", spec.import_ops.iter(), |op| {
229-
format!("Import: name={}, {}", op.name, op.spec.format(mode))
230-
});
221+
for op in &spec.import_ops {
222+
sections
223+
.entry("Sources".to_string())
224+
.or_default()
225+
.push(RenderedSpecLine {
226+
content: format!("Import: name={}, {}", op.name, op.spec.format(mode)),
227+
scope: None,
228+
children: vec![],
229+
});
230+
}
231231

232232
// Processing
233233
fn walk(
234234
op: &NamedSpec<ReactiveOpSpec>,
235-
indent: u32,
236235
mode: OutputMode,
237-
out: &mut Vec<(String, String, u32)>,
238-
) {
239-
out.push((
240-
"Processing".into(),
241-
format!("{}: {}", op.name, op.spec.format(mode)),
242-
indent,
243-
));
236+
scope: Option<String>,
237+
) -> RenderedSpecLine {
238+
let content = format!("{}: {}", op.name, op.spec.format(mode));
239+
let mut line = RenderedSpecLine {
240+
content,
241+
scope,
242+
children: vec![],
243+
};
244244

245245
if let ReactiveOpSpec::ForEach(fe) = &op.spec {
246-
out.push(("Processing".into(), fe.op_scope.to_string(), indent + 1));
247246
for nested in &fe.op_scope.ops {
248-
walk(nested, indent + 2, mode, out);
247+
line.children
248+
.push(walk(nested, mode, Some(fe.op_scope.name.clone())));
249249
}
250250
}
251+
252+
line
251253
}
252254

253255
for op in &spec.reactive_ops {
254-
walk(op, 0, mode, &mut result);
256+
sections
257+
.entry("Processing".to_string())
258+
.or_default()
259+
.push(walk(op, mode, None));
255260
}
256261

257262
// Targets
258-
extend_with(&mut result, "Targets", spec.export_ops.iter(), |op| {
259-
format!("Export: name={}, {}", op.name, op.spec.format(mode))
260-
});
263+
for op in &spec.export_ops {
264+
sections
265+
.entry("Targets".to_string())
266+
.or_default()
267+
.push(RenderedSpecLine {
268+
content: format!("Export: name={}, {}", op.name, op.spec.format(mode)),
269+
scope: None,
270+
children: vec![],
271+
});
272+
}
261273

262274
// Declarations
263-
extend_with(
264-
&mut result,
265-
"Declarations",
266-
spec.declarations.iter(),
267-
|decl| format!("Declaration: {}", decl.format(mode)),
268-
);
275+
for decl in &spec.declarations {
276+
sections
277+
.entry("Declarations".to_string())
278+
.or_default()
279+
.push(RenderedSpecLine {
280+
content: format!("Declaration: {}", decl.format(mode)),
281+
scope: None,
282+
children: vec![],
283+
});
284+
}
269285

270-
Ok(result)
286+
Ok(RenderedSpec {
287+
sections: sections.into_iter().collect(),
288+
})
271289
}
272290

273291
pub fn get_schema(&self) -> Vec<(String, String, String)> {
@@ -518,6 +536,8 @@ fn cocoindex_engine(m: &Bound<'_, PyModule>) -> PyResult<()> {
518536
m.add_class::<SimpleSemanticsQueryHandler>()?;
519537
m.add_class::<SetupStatusCheck>()?;
520538
m.add_class::<PyOpArgSchema>()?;
539+
m.add_class::<RenderedSpec>()?;
540+
m.add_class::<RenderedSpecLine>()?;
521541

522542
Ok(())
523543
}

0 commit comments

Comments
 (0)