-
Notifications
You must be signed in to change notification settings - Fork 21
feat: AST flow inspection, parameter prompts, and Copilot follow-ups #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
21526c3
b38be24
4c7e8e0
9a35372
e612580
8a7ba15
8572cbb
f95754b
8a25018
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| /** Metaflow/Click long-option style: letters, digits, underscore, hyphen (not whitespace or '='). */ | ||
| const CLI_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_-]*$/; | ||
|
|
||
| /** | ||
| * Single-quote for bash/zsh: prevents all expansion. | ||
| * Only ' needs escaping, done by ending the string, adding an escaped | ||
| * literal quote, and reopening: it's → 'it'\''s' | ||
| */ | ||
| function quoteForBash(value) { | ||
| return "'" + value.replace(/'/g, "'\\''" ) + "'"; | ||
| } | ||
|
|
||
| /** | ||
| * Single-quote for PowerShell: prevents all expansion. | ||
| * Only ' needs escaping, done by doubling: it's → 'it''s' | ||
| */ | ||
| function quoteForPowershell(value) { | ||
| return "'" + value.replace(/'/g, "''" ) + "'"; | ||
| } | ||
|
|
||
| /** | ||
| * Double-quote for cmd.exe: the only option since cmd has no single-quote | ||
| * string semantics. Escape " by doubling and % by doubling. | ||
| */ | ||
| function quoteForCmd(value) { | ||
| return '"' + value.replace(/"/g, '""').replace(/%/g, '%%') + '"'; | ||
| } | ||
|
|
||
| /** | ||
| * Quote a string for safe use in a terminal command. | ||
| * Uses single quotes for bash/PowerShell (prevents all variable expansion) | ||
| * and double quotes for cmd.exe. | ||
| * | ||
| * @param {string} value - The raw string to quote. | ||
| * @param {'bash'|'powershell'|'cmd'} shell - Target shell type. | ||
| */ | ||
| function quoteForTerminal(value, shell) { | ||
| if (shell === 'powershell') return quoteForPowershell(value); | ||
| if (shell === 'cmd') return quoteForCmd(value); | ||
| return quoteForBash(value); | ||
| } | ||
|
|
||
| /** | ||
| * Detect shell type from a shell executable path. | ||
| * @param {string} shellPath - e.g. '/bin/bash' or 'C:\\...\\powershell.exe' | ||
| * @returns {'bash'|'powershell'|'cmd'} | ||
| */ | ||
| function detectShellType(shellPath) { | ||
| const lower = (shellPath || '').toLowerCase(); | ||
| if (lower.includes('powershell') || lower.includes('pwsh')) return 'powershell'; | ||
| if (lower.endsWith('cmd.exe') || lower.endsWith('cmd')) return 'cmd'; | ||
| return 'bash'; | ||
| } | ||
|
|
||
| /** | ||
| * Build a `python <file> run --flag=value ...` command string. | ||
| * | ||
| * @param {string} filePath - Absolute path to the flow file. | ||
| * @param {Array<{name: string, value: string}>} flagArgs - Parameter name/value pairs. | ||
| * @param {'bash'|'powershell'|'cmd'} shell - Target shell type. | ||
| * @returns {string} The shell command to execute. | ||
| */ | ||
| function buildRunCommand(filePath, flagArgs, shell) { | ||
| const parts = ['python', quoteForTerminal(filePath, shell), 'run']; | ||
|
|
||
| for (const arg of flagArgs) { | ||
| if (!CLI_NAME_RE.test(arg.name)) { | ||
| throw new Error(`Invalid parameter name: ${arg.name}`); | ||
| } | ||
| if (/[\r\n]/.test(arg.value)) { | ||
| throw new Error(`Parameter value for --${arg.name} contains newline characters.`); | ||
| } | ||
| parts.push(`--${arg.name}=${quoteForTerminal(arg.value, shell)}`); | ||
| } | ||
|
|
||
| return parts.join(' '); | ||
| } | ||
|
|
||
| module.exports = { | ||
| buildRunCommand, | ||
| quoteForTerminal, | ||
| quoteForBash, | ||
| quoteForPowershell, | ||
| quoteForCmd, | ||
| detectShellType, | ||
| CLI_NAME_RE, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| """ | ||
| Example Metaflow flow for manual testing of the VS Code extension. | ||
|
|
||
| To test parameter prompts: | ||
| 1. Open this file in VS Code with the extension installed | ||
| 2. Press Ctrl+Alt+R to run the flow | ||
| 3. You should see input prompts for 'greeting' and 'count' | ||
| pre-filled with their defaults | ||
| 4. The final terminal command should look like: | ||
| python hello_flow.py run --greeting='...' --count='...' | ||
|
|
||
| To test spin: | ||
| 1. Place your cursor inside the 'start' or 'end' method | ||
| 2. Press Ctrl+Alt+S | ||
| """ | ||
|
|
||
| from metaflow import FlowSpec, Parameter, step | ||
|
|
||
|
|
||
| class HelloFlow(FlowSpec): | ||
| greeting = Parameter('greeting', default='hello world', type=str, help='Message to print') | ||
| count = Parameter('count', default=3, type=int, help='How many times to repeat') | ||
|
|
||
| @step | ||
| def start(self): | ||
| for i in range(self.count): | ||
| print(f"{i + 1}: {self.greeting}") | ||
| self.next(self.end) | ||
|
|
||
| @step | ||
| def end(self): | ||
| print("Done!") | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| HelloFlow() |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||||||||||||||
| /** | ||||||||||||||||||
| * Pure predicate: whether AST inspection allows run/spin. | ||||||||||||||||||
| * @param {{ syntaxOk: boolean, hasFlowSpec: boolean }} inspectResult | ||||||||||||||||||
| */ | ||||||||||||||||||
| function flowInspectAllowsRun(inspectResult) { | ||||||||||||||||||
| return !!(inspectResult && inspectResult.syntaxOk && inspectResult.hasFlowSpec); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Validates the active editor: must exist and be a Python document. | ||||||||||||||||||
| * Shows an error message and returns null when validation fails. | ||||||||||||||||||
| * @returns {import('vscode').TextDocument | null} | ||||||||||||||||||
| */ | ||||||||||||||||||
| function validateEditorForFlowCommands(editor) { | ||||||||||||||||||
| const vscode = require('vscode'); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!editor) { | ||||||||||||||||||
| vscode.window.showErrorMessage('No active editor.'); | ||||||||||||||||||
| return null; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| const doc = editor.document; | ||||||||||||||||||
| if (doc.languageId !== 'python') { | ||||||||||||||||||
| vscode.window.showErrorMessage('Active file is not a Python file.'); | ||||||||||||||||||
| return null; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return doc; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Shows errors when inspect result blocks run/spin. | ||||||||||||||||||
| * @returns {boolean} true if run/spin should abort (failure reported), false if OK to continue. | ||||||||||||||||||
| * @param {{ syntaxOk: boolean, hasFlowSpec: boolean, error?: string | null }} inspectResult | ||||||||||||||||||
| */ | ||||||||||||||||||
| function reportFlowInspectFailure(inspectResult) { | ||||||||||||||||||
| const vscode = require('vscode'); | ||||||||||||||||||
|
|
||||||||||||||||||
| if (flowInspectAllowsRun(inspectResult)) { | ||||||||||||||||||
| return false; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
||||||||||||||||||
| if (!inspectResult) { | |
| vscode.window.showErrorMessage( | |
| 'Flow inspection failed.' | |
| ); | |
| return true; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Windows when the integrated terminal shell is cmd.exe,
cdwill not switch drives (e.g. C: -> D:) unless/dis used. This can cause the subsequentpython ...command to run in the wrong directory for flows located on a different drive. Consider emittingcd /d <dir>forshell === 'cmd'(or separately sending a<drive>:command beforecd).