Skip to content

Commit 058a20b

Browse files
committed
Add parameter overrides to import magic
Support passing parameter overrides to #!import via a new --param flag. Adds a CLI-style parser (ParseArguments) and ResolveAndInjectParameters to coerce/validate overrides, merge them with imported notebook defaults, inject values into the variable store, and enforce required params (stopping execution with an error if missing). Updates command metadata/usage and error text, and expands tests to cover argument parsing, type coercion, validation, and integration scenarios where parameters are applied before executing code cells.
1 parent c14333f commit 058a20b

2 files changed

Lines changed: 491 additions & 8 deletions

File tree

src/Verso/MagicCommands/ImportMagicCommand.cs

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using Verso.Abstractions;
22
using Verso.Extensions;
3+
using Verso.Parameters;
34

45
namespace Verso.MagicCommands;
56

67
/// <summary>
7-
/// <c>#!import path</c> — reads a notebook file, deserializes it, and executes all code cells
8-
/// in the current kernel session. Variables and state persist for subsequent cells.
8+
/// <c>#!import path [--param name=value ...]</c> — reads a notebook file, deserializes it, resolves
9+
/// parameters against the imported notebook's definitions, and executes all code cells in the current
10+
/// kernel session. Variables and state persist for subsequent cells.
911
/// </summary>
1012
[VersoExtension]
1113
public sealed class ImportMagicCommand : IMagicCommand
@@ -20,11 +22,12 @@ public sealed class ImportMagicCommand : IMagicCommand
2022
// --- IMagicCommand ---
2123

2224
public string Name => "import";
23-
public string Description => "Imports and executes all code cells from another notebook file.";
25+
public string Description => "Imports and executes all code cells from another notebook file, with optional parameter overrides.";
2426

2527
public IReadOnlyList<ParameterDefinition> Parameters { get; } = new[]
2628
{
27-
new ParameterDefinition("path", "Path to the notebook file to import.", typeof(string), IsRequired: true)
29+
new ParameterDefinition("path", "Path to the notebook file to import.", typeof(string), IsRequired: true),
30+
new ParameterDefinition("--param", "Parameter override in name=value format. May be repeated.", typeof(string), IsRequired: false)
2831
};
2932

3033
public Task OnLoadedAsync(IExtensionHostContext context) => Task.CompletedTask;
@@ -37,12 +40,12 @@ public async Task ExecuteAsync(string arguments, IMagicCommandContext context)
3740
if (string.IsNullOrWhiteSpace(arguments))
3841
{
3942
await context.WriteOutputAsync(new CellOutput("text/plain",
40-
"Error: #!import requires a file path. Usage: #!import <path>", IsError: true))
41-
.ConfigureAwait(false);
43+
"Error: #!import requires a file path. Usage: #!import <path> [--param name=value ...]",
44+
IsError: true)).ConfigureAwait(false);
4245
return;
4346
}
4447

45-
var path = arguments.Trim();
48+
var (path, paramOverrides) = ParseArguments(arguments);
4649

4750
try
4851
{
@@ -90,6 +93,19 @@ await context.WriteOutputAsync(new CellOutput("text/plain",
9093
}
9194
}
9295

96+
// Resolve parameters: explicit --param overrides first (with type
97+
// coercion and validation), then fill remaining defaults from the
98+
// imported notebook's definitions. Explicit overrides always win.
99+
var paramError = ResolveAndInjectParameters(
100+
notebook, paramOverrides, context.Variables);
101+
102+
if (paramError is not null)
103+
{
104+
await context.WriteOutputAsync(new CellOutput("text/plain",
105+
paramError, IsError: true)).ConfigureAwait(false);
106+
return;
107+
}
108+
93109
var codeCellCount = 0;
94110
foreach (var cell in notebook.Cells)
95111
{
@@ -119,6 +135,102 @@ await context.WriteOutputAsync(new CellOutput("text/plain",
119135
}
120136
}
121137

138+
/// <summary>
139+
/// Parses the arguments string into a file path and optional --param overrides.
140+
/// </summary>
141+
internal static (string Path, Dictionary<string, string> Params) ParseArguments(string arguments)
142+
{
143+
var parts = arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries);
144+
var path = parts[0];
145+
var paramOverrides = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
146+
147+
for (var i = 1; i < parts.Length; i++)
148+
{
149+
if (parts[i] is "--param" or "-p" && i + 1 < parts.Length)
150+
{
151+
var pair = parts[++i];
152+
var eq = pair.IndexOf('=');
153+
if (eq > 0)
154+
paramOverrides[pair[..eq]] = pair[(eq + 1)..];
155+
}
156+
}
157+
158+
return (path, paramOverrides);
159+
}
160+
161+
/// <summary>
162+
/// Resolves --param overrides and imported notebook defaults, validates required
163+
/// parameters, and merges everything into the variable store. Returns an error
164+
/// message if required parameters are missing, or null on success.
165+
/// </summary>
166+
internal static string? ResolveAndInjectParameters(
167+
NotebookModel notebook,
168+
Dictionary<string, string> paramOverrides,
169+
IVariableStore variables)
170+
{
171+
var definitions = notebook.Parameters;
172+
173+
// If the imported notebook has no parameter definitions, inject any
174+
// overrides as untyped strings and return.
175+
if (definitions is not { Count: > 0 })
176+
{
177+
foreach (var (name, value) in paramOverrides)
178+
variables.Set(name, value);
179+
return null;
180+
}
181+
182+
// 1. Apply explicit --param overrides with type coercion.
183+
foreach (var (name, raw) in paramOverrides)
184+
{
185+
if (definitions.TryGetValue(name, out var def))
186+
{
187+
if (ParameterValueParser.TryParse(def.Type, raw, out var typed, out var error) && typed is not null)
188+
variables.Set(name, typed);
189+
else
190+
return $"Error: Invalid value for parameter '{name}' ({def.Type}): {error}";
191+
}
192+
else
193+
{
194+
// Unknown parameter -- inject as string (matches CLI behavior).
195+
variables.Set(name, raw);
196+
}
197+
}
198+
199+
// 2. Fill defaults for parameters not already in the store.
200+
foreach (var (name, def) in definitions)
201+
{
202+
if (variables.TryGet<object>(name, out var existing) && existing is not null)
203+
continue;
204+
205+
if (def.Default is null)
206+
continue;
207+
208+
var value = def.Default;
209+
if (value is string str && def.Type is not "string")
210+
{
211+
if (ParameterValueParser.TryParse(def.Type, str, out var parsed, out _) && parsed is not null)
212+
value = parsed;
213+
}
214+
215+
variables.Set(name, value);
216+
}
217+
218+
// 3. Validate required parameters.
219+
var missing = new List<string>();
220+
foreach (var (name, def) in definitions)
221+
{
222+
if (!def.Required) continue;
223+
if (variables.TryGet<object>(name, out var val) && val is not null) continue;
224+
missing.Add($" {name} ({def.Type}){(def.Description is not null ? " -- " + def.Description : "")}");
225+
}
226+
227+
if (missing.Count > 0)
228+
return $"Error: Missing required parameter{(missing.Count > 1 ? "s" : "")} " +
229+
$"for imported notebook:\n{string.Join("\n", missing)}";
230+
231+
return null;
232+
}
233+
122234
/// <summary>
123235
/// Resolves a file path relative to the notebook's directory, or the current working directory
124236
/// if no notebook path is available.

0 commit comments

Comments
 (0)