11using Verso . Abstractions ;
22using Verso . Extensions ;
3+ using Verso . Parameters ;
34
45namespace 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 ]
1113public 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