Skip to content

feat: add support for Prefect 3.0 pipelines#26

Open
Resmung0 wants to merge 2 commits into
masterfrom
feature/prefect-support-15643003390712396561
Open

feat: add support for Prefect 3.0 pipelines#26
Resmung0 wants to merge 2 commits into
masterfrom
feature/prefect-support-15643003390712396561

Conversation

@Resmung0

@Resmung0 Resmung0 commented May 9, 2026

Copy link
Copy Markdown
Owner

This PR adds support for Prefect 3.0 pipelines to the Caldera extension.

Key changes:

  • Created PrefectParser in src/extension/parsers/data-processing/ which uses static analysis (regex) to identify @flow and @task decorated functions in Python files.
  • The parser detects dependencies between tasks by tracking variable assignments from task returns (including .submit() calls) and checking wait_for parameters.
  • Registered the PrefectParser in the DataProcessingPipeline for **/*.py files.
  • Added comprehensive unit tests in tests/PrefectParser.test.ts to verify flow detection and dependency resolution.
  • Updated package.json to include ts-jest for running TypeScript-based unit tests.

The implementation follows the established pattern for data processing parsers and integrates with the existing visualization infrastructure.


PR created automatically by Jules for task 15643003390712396561 started by @Resmung0

Summary by Sourcery

Add a Prefect data-processing parser and register it in the pipeline to visualize Prefect-based Python workflows, alongside updating testing dependencies.

New Features:

  • Introduce a PrefectParser to detect Prefect flows and tasks in Python files and extract their task dependency graph for visualization.

Enhancements:

  • Register the PrefectParser in the DataProcessingPipeline so Prefect workflows in any Python file are processed.
  • Add unit tests covering PrefectParser flow detection and task dependency resolution, including direct and submitted task calls.

Build:

  • Bump ts-jest dependency to the latest patch version in package.json.

- Implement PrefectParser to detect and parse Prefect flows and tasks from Python files.
- Support task dependencies via data passing and `wait_for` arguments.
- Register PrefectParser in DataProcessingPipeline.
- Add unit tests for PrefectParser.
- Add ts-jest to devDependencies to support TypeScript tests.

Co-authored-by: Resmung0 <36093882+Resmung0@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-ai

sourcery-ai Bot commented May 9, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new Prefect 3.0 data-processing parser and integrates it into the existing pipeline infrastructure, including unit tests and a minor test tooling dependency bump.

Sequence diagram for PrefectParser handling a Prefect 3.0 Python file

sequenceDiagram
  participant VSCodeExtension
  participant DataProcessingPipeline
  participant PrefectParser
  participant VisualizationEngine

  VSCodeExtension->>DataProcessingPipeline: analyzeWorkspace()
  loop for each file
    DataProcessingPipeline->>PrefectParser: canParse(fileName, content)
    alt file is Prefect flow
      PrefectParser-->>DataProcessingPipeline: true
      DataProcessingPipeline->>PrefectParser: parse(content, filePath)
      PrefectParser->>PrefectParser: extract taskNames via taskRegex
      PrefectParser->>PrefectParser: extract flowBody via flowRegex
      PrefectParser->>PrefectParser: build nodes and edges
      PrefectParser-->>DataProcessingPipeline: PipelineData
      DataProcessingPipeline->>VisualizationEngine: registerPipeline(PipelineData)
    else not a Prefect file
      PrefectParser-->>DataProcessingPipeline: false
    end
  end
Loading

Class diagram for PrefectParser integration into DataProcessingPipeline

classDiagram
  class DataProcessingPipeline {
    +PipelinePatternType type
    +IParser[] parsers
  }

  class IParser {
    <<interface>>
    +string name
    +canParse(fileName string, content string) boolean
    +parse(content string, filePath string) Promise~PipelineData~
  }

  class PrefectParser {
    +string name
    +canParse(fileName string, content string) boolean
    +parse(content string, filePath string) Promise~PipelineData~
  }

  class AirflowParser {
    +string name
    +canParse(fileName string, content string) boolean
    +parse(content string, filePath string) Promise~PipelineData~
  }

  class KedroParser {
    +string name
    +canParse(fileName string, content string) boolean
    +parse(content string, filePath string) Promise~PipelineData~
  }

  class DVCParser {
    +string name
    +canParse(fileName string, content string) boolean
    +parse(content string, filePath string) Promise~PipelineData~
  }

  class PipelineData {
    +string filePath
    +string framework
    +PipelineNode[] nodes
    +PipelineEdge[] edges
  }

  class PipelineNode {
    +string id
    +string label
    +string type
    +any data
  }

  class PipelineEdge {
    +string id
    +string source
    +string target
  }

  DataProcessingPipeline o-- IParser : uses
  DataProcessingPipeline o-- PrefectParser : parser
  DataProcessingPipeline o-- AirflowParser : parser
  DataProcessingPipeline o-- KedroParser : parser
  DataProcessingPipeline o-- DVCParser : parser

  PrefectParser ..|> IParser
  AirflowParser ..|> IParser
  KedroParser ..|> IParser
  DVCParser ..|> IParser

  PrefectParser --> PipelineData
  PipelineData --> PipelineNode
  PipelineData --> PipelineEdge
Loading

Flow diagram for PrefectParser parse logic and dependency detection

flowchart TD
  A["Start parse(content, filePath)"] --> B["Initialize empty nodes and edges"]
  B --> C["Find @task decorated functions with taskRegex"]
  C --> D["For each taskName: add PipelineNode and store in taskNames"]
  D --> E["Find first @flow definition and extract flowBody with flowRegex"]
  E --> F{Flow found?}
  F -- "No" --> G["Return PipelineData with only task nodes"]
  F -- "Yes" --> H["Initialize varToTask map"]
  H --> I["Split flowBody into lines"]
  I --> J["Iterate lines and taskNames"]
  J --> K["Match assignments: var = taskName.submit(...) or var = taskName(...)"]
  K --> L["On match: map variable to taskName in varToTask"]
  L --> M["Match calls: taskName.submit(args) or taskName(args)"]
  M --> N{callMatch exists?}
  N -- "No" --> J
  N -- "Yes" --> O["Inspect args for variables present in varToTask"]
  O --> P["For each matching variable: add edge sourceTask -> taskName if not exists"]
  P --> Q["Inspect args for nested calls otherTask(')"]
  Q --> R["For each otherTask in taskNames: if args contains otherTask( add edge otherTask -> taskName"]
  R --> J
  G --> S["Return PipelineData(filePath, framework, nodes, edges)"]
  J --> T["All lines processed"]
  T --> S
Loading

File-Level Changes

Change Details Files
Introduce a Prefect-specific data processing parser that statically analyzes Python flows/tasks and derives task dependency graphs.
  • Implement PrefectParser with canParse that gates on .py extension plus Prefect imports and @flow usage.
  • Use regex-based scanning to collect @task-decorated function names and emit corresponding pipeline nodes with Prefect framework metadata.
  • Detect the main @flow function body and iterate line-by-line to infer variable-to-task mappings from assignments involving task calls or .submit() calls.
  • Within the flow body, analyze task invocations to create edges when downstream task arguments reference variables produced by upstream tasks or directly call other task functions (including nested calls).
  • Return a PipelineData object including filePath, framework name, and the derived nodes/edges graph.
src/extension/parsers/data-processing/PrefectParser.ts
Register the Prefect parser in the data processing pipeline so it runs over Python files.
  • Import PrefectParser into DataProcessingPipeline.
  • Append a PrefectParser instance with a '**/*.py' glob pattern to the pipeline's parsers array so all Python files are considered for Prefect detection.
src/extension/pipelines/DataProcessingPipeline.ts
Add unit tests to validate Prefect parser detection and dependency resolution behavior.
  • Test canParse to ensure it only matches .py files that both import Prefect and define a @flow.
  • Test parsing of a representative flow containing tasks with .submit(), positional dependencies, and wait_for=[...] to confirm nodes and edges are produced as expected.
  • Test that direct nested task calls (task_b(task_a())) are recognized as dependencies.
tests/PrefectParser.test.ts
Update ts-jest dependency used for running TypeScript-based Jest tests.
  • Bump ts-jest version from 29.4.6 to 29.4.9 in package.json and propagate the corresponding change in package-lock.json.
package.json
package-lock.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • The PrefectParser currently only processes the first @flow match in a file (flowRegex.exec used once), so additional flows in the same module will be ignored; consider iterating over all matches to capture multiple flows.
  • The canParse heuristic (content.includes("from prefect import") && content.includes("@flow")) will miss valid Prefect 3 flows that use other import styles (e.g., import prefect, from prefect import flow without task, or from prefect import flow as f); consider loosening this detection to rely on broader prefect imports and/or decorator patterns.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `PrefectParser` currently only processes the first `@flow` match in a file (`flowRegex.exec` used once), so additional flows in the same module will be ignored; consider iterating over all matches to capture multiple flows.
- The `canParse` heuristic (`content.includes("from prefect import") && content.includes("@flow")`) will miss valid Prefect 3 flows that use other import styles (e.g., `import prefect`, `from prefect import flow` without `task`, or `from prefect import flow as f`); consider loosening this detection to rely on broader `prefect` imports and/or decorator patterns.

## Individual Comments

### Comment 1
<location path="src/extension/parsers/data-processing/PrefectParser.ts" line_range="35-36" />
<code_context>
+    }
+
+    // 2. Identify the flow and its body
+    const flowRegex = /@flow(?:\([^)]*\))?\s+def\s+(\w+)\s*\([^)]*\):([\s\S]+?)(?=\n\S|$)/g;
+    const flowMatch = flowRegex.exec(content);
+    if (flowMatch) {
+      const flowBody = flowMatch[2];
</code_context>
<issue_to_address>
**issue (bug_risk):** Only the first flow definition is parsed even though the regex is global.

`flowRegex` uses the `g` flag but is only executed once, so only the first `@flow` is handled and later flows are skipped. If multiple flows per file are expected, loop over the matches (e.g. `while ((flowMatch = flowRegex.exec(content)) !== null) { ... }`) and aggregate nodes/edges for each flow.
</issue_to_address>

### Comment 2
<location path="src/extension/parsers/data-processing/PrefectParser.ts" line_range="61-64" />
<code_context>
+          const callRegex = new RegExp(`${taskName}(?:\\.submit)?\\s*\\(([^)]*)\\)`);
+          const callMatch = line.match(callRegex);
+          if (callMatch) {
+            const args = callMatch[1];
+
+            // Check for variables in args that map to tasks
+            varToTask.forEach((sourceTask, variable) => {
+              if (args.includes(variable)) {
+                const edgeId = `e-${sourceTask}-${taskName}`;
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Variable-to-task matching via `includes` can cause false-positive edges.

`args.includes(variable)` risks matching substrings or content inside string literals (e.g., variable `a` matching `data`), which can produce spurious dependencies. It would be safer to parse the argument list (e.g., splitting into identifiers) or at least match with word boundaries via a regex (e.g., `new RegExp(`\b${variable}\b`)`).
</issue_to_address>

### Comment 3
<location path="tests/PrefectParser.test.ts" line_range="17" />
<code_context>
+    expect(parser.canParse('flow.txt', content)).toBe(false);
+  });
+
+  it('should parse tasks and dependencies from a flow', async () => {
+    const content = `
+from prefect import flow, task
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test case for dependencies created via plain task calls (without `.submit()`), since the parser supports both.

The assignment regex already supports both `task_name.submit(...)` and `task_name(...)`, but the current dependency test only exercises `a = task_a.submit()`. Please add coverage for the plain-call style—e.g., `a = task_a()` and `b = task_b(a)`—and assert the same edge structure, so we don’t regress support for non-`.submit()` calls.

Suggested implementation:

```typescript
  it('should parse tasks and dependencies from a flow', async () => {
    const content = `
from prefect import flow, task

@task
def task_a():
    return "a"

@task
def task_b(data):
    return "b"

@task
def task_c():
    return "c"

@flow
def submit_style_flow():
    a = task_a.submit()
    b = task_b.submit(a)
    c = task_c.submit()
    return b

@flow
def direct_call_flow():
    a = task_a()
    b = task_b(a)
    c = task_c()
    return b


```

The updated flow content now includes **both** dependency styles:
- `submit_style_flow` uses `task_a.submit(...)` / `task_b.submit(a)`.
- `direct_call_flow` uses plain calls `task_a(...)` / `task_b(a)`.

To fully implement the test intent, you should verify that the parser identifies edges for *both* styles. Depending on how `PrefectParser.parse` currently reports tasks/edges, youll likely need to:
1. Ensure the expectations in this test assert that there is an edge from `task_a` to `task_b` created from:
   - The `.submit()` call, and
   - The direct call.
2. If the parser groups all edges together without distinguishing by flow function, the test can simply assert that there is at least one `task_a -> task_b` edge for each style (or that there are two such edges, if the parser does not deduplicate).

Adjust the expectations in this test accordingly using your existing assertion pattern for dependencies. If youd prefer truly separate test cases (one for `.submit` and one for direct calls), you can split this combined flow into two distinct tests, each using only one of the two flow definitions above, and reuse the same expectations in both tests.
</issue_to_address>

### Comment 4
<location path="src/extension/parsers/data-processing/PrefectParser.ts" line_range="15" />
<code_context>
+    );
+  }
+
+  async parse(content: string, filePath: string): Promise<PipelineData> {
+    const nodes: PipelineNode[] = [];
+    const edges: PipelineEdge[] = [];
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the parse logic into small helper methods with separate passes for collecting assignments and building edges while reusing precomputed regexes instead of creating them per line.

You can keep the behavior but reduce complexity by:
- splitting the `parse` logic into small helpers, and
- separating “assignment collection” from “edge extraction,”
- avoiding per-line, per-task regex construction.

### 1. Extract helpers for clarity

Move task and flow extraction into dedicated functions so `parse` reads as a high-level pipeline:

```ts
async parse(content: string, filePath: string): Promise<PipelineData> {
  const nodes: PipelineNode[] = [];
  const edges: PipelineEdge[] = [];

  const taskNames = this.extractTasks(content, nodes);
  const flowBody = this.extractFlowBody(content);

  if (flowBody) {
    const lines = flowBody.split('\n');
    const varToTask = this.buildVarToTaskMap(lines, taskNames);
    this.extractEdgesFromLines(lines, taskNames, varToTask, edges);
  }

  return { filePath, framework: this.name, nodes, edges };
}

private extractTasks(content: string, nodes: PipelineNode[]): string[] {
  const taskRegex = /@task(?:\([^)]*\))?\s+def\s+(\w+)/g;
  const taskNames: string[] = [];
  let match;
  while ((match = taskRegex.exec(content)) !== null) {
    const name = match[1];
    taskNames.push(name);
    nodes.push({ id: name, label: name, type: 'default', data: { framework: this.name } });
  }
  return taskNames;
}

private extractFlowBody(content: string): string | null {
  const flowRegex = /@flow(?:\([^)]*\))?\s+def\s+(\w+)\s*\([^)]*\):([\s\S]+?)(?=\n\S|$)/g;
  const flowMatch = flowRegex.exec(content);
  return flowMatch ? flowMatch[2] : null;
}
```

### 2. Two passes: assignments vs edges

This reduces interleaving state/sideeffects and removes one level of nesting:

```ts
private buildVarToTaskMap(lines: string[], taskNames: string[]): Map<string, string> {
  const varToTask = new Map<string, string>();

  // Precompute assignment regexes per task
  const assignmentRegexes = taskNames.map(
    t => ({ task: t, re: new RegExp(`(\\w+)\\s*=\\s*${t}(?:\\.submit)?\\s*\\(`) })
  );

  for (const line of lines) {
    for (const { task, re } of assignmentRegexes) {
      const m = line.match(re);
      if (m) {
        varToTask.set(m[1], task);
      }
    }
  }
  return varToTask;
}

private extractEdgesFromLines(
  lines: string[],
  taskNames: string[],
  varToTask: Map<string, string>,
  edges: PipelineEdge[]
): void {
  // Precompute call regexes per task
  const callRegexes = taskNames.map(
    t => ({ task: t, re: new RegExp(`${t}(?:\\.submit)?\\s*\\(([^)]*)\\)`) })
  );

  const ensureEdge = (source: string, target: string) => {
    const id = `e-${source}-${target}`;
    if (!edges.some(e => e.id === id)) {
      edges.push({ id, source, target });
    }
  };

  for (const line of lines) {
    for (const { task: targetTask, re } of callRegexes) {
      const callMatch = line.match(re);
      if (!callMatch) continue;

      const args = callMatch[1];

      // vars -> tasks
      varToTask.forEach((sourceTask, variable) => {
        if (args.includes(variable)) {
          ensureEdge(sourceTask, targetTask);
        }
      });

      // direct nested calls: task2(task1())
      for (const otherTask of taskNames) {
        if (otherTask !== targetTask && args.includes(`${otherTask}(`)) {
          ensureEdge(otherTask, targetTask);
        }
      }
    }
  }
}
```

This preserves the current behavior but:
- removes perline regex allocation,
- isolates responsibilities into small, testable helpers,
- and simplifies control flow (no mutation of `varToTask` while resolving dependencies).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/extension/parsers/data-processing/PrefectParser.ts Outdated
Comment thread src/extension/parsers/data-processing/PrefectParser.ts Outdated
Comment thread tests/PrefectParser.test.ts Outdated
expect(parser.canParse('flow.txt', content)).toBe(false);
});

it('should parse tasks and dependencies from a flow', async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add a test case for dependencies created via plain task calls (without .submit()), since the parser supports both.

The assignment regex already supports both task_name.submit(...) and task_name(...), but the current dependency test only exercises a = task_a.submit(). Please add coverage for the plain-call style—e.g., a = task_a() and b = task_b(a)—and assert the same edge structure, so we don’t regress support for non-.submit() calls.

Suggested implementation:

  it('should parse tasks and dependencies from a flow', async () => {
    const content = `
from prefect import flow, task

@task
def task_a():
    return "a"

@task
def task_b(data):
    return "b"

@task
def task_c():
    return "c"

@flow
def submit_style_flow():
    a = task_a.submit()
    b = task_b.submit(a)
    c = task_c.submit()
    return b

@flow
def direct_call_flow():
    a = task_a()
    b = task_b(a)
    c = task_c()
    return b

The updated flow content now includes both dependency styles:

  • submit_style_flow uses task_a.submit(...) / task_b.submit(a).
  • direct_call_flow uses plain calls task_a(...) / task_b(a).

To fully implement the test intent, you should verify that the parser identifies edges for both styles. Depending on how PrefectParser.parse currently reports tasks/edges, you’ll likely need to:

  1. Ensure the expectations in this test assert that there is an edge from task_a to task_b created from:
    • The .submit() call, and
    • The direct call.
  2. If the parser groups all edges together without distinguishing by flow function, the test can simply assert that there is at least one task_a -> task_b edge for each style (or that there are two such edges, if the parser does not deduplicate).

Adjust the expectations in this test accordingly using your existing assertion pattern for dependencies. If you’d prefer truly separate test cases (one for .submit and one for direct calls), you can split this combined flow into two distinct tests, each using only one of the two flow definitions above, and reuse the same expectations in both tests.

Comment thread src/extension/parsers/data-processing/PrefectParser.ts
- Support multiple flows per file.
- Improve 'canParse' to handle various import and decorator styles (e.g., @prefect.flow, @f).
- Refactor 'parse' into helper methods for better maintainability and performance.
- Use word boundaries in variable matching to avoid false-positive dependencies.
- Optimize regex usage by precomputing them.
- Add comprehensive unit tests for new features and edge cases.

Co-authored-by: Resmung0 <36093882+Resmung0@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant