2727from ipylab .shell import Shell
2828
2929if TYPE_CHECKING :
30+ from collections .abc import Iterable
3031 from typing import ClassVar
3132
3233
@@ -129,52 +130,9 @@ async def _do_operation_for_frontend(self, operation: str, payload: dict, buffer
129130 if not isinstance (widget , Widget ):
130131 msg = f"Expected an Widget but got { type (widget )} "
131132 raise TypeError (msg )
132- result ["payload" ] = await self .shell .add (widget , ** payload )
133- return result
134- return await super ()._do_operation_for_frontend (operation , payload , buffers )
135-
136- async def _evaluate (self , options : dict , buffers : list ):
137- """Evaluate code.
133+ return await self .shell .add (widget , ** payload )
138134
139- A call to this method should originate from either:
140- 1. An `evaluate` method call from a subclass of `Ipylab` from kernel via
141- `jfem.evaluate` in the frontend.
142- 2. A call in the frontend to `jfem.evaluate`.
143- """
144- evaluate = options ["evaluate" ]
145- if isinstance (evaluate , str ):
146- evaluate = {"payload" : evaluate }
147- namespace_id = options .get ("namespace_id" , "" )
148- ns = self .get_namespace (namespace_id , buffers = buffers )
149- for name , expression in evaluate .items ():
150- try :
151- result = eval (expression , ns ) # noqa: S307
152- except SyntaxError :
153- exec (expression , ns ) # noqa: S102
154- result = next (reversed (ns .values ())) # Requires: LastUpdatedDict
155- while callable (result ) or inspect .isawaitable (result ):
156- if callable (result ):
157- kwgs = {}
158- for p in inspect .signature (result ).parameters :
159- if p in options :
160- kwgs [p ] = options [p ]
161- if p in ns :
162- kwgs [p ] = ns [p ]
163- # We use a partial so that we can evaluate with the same namespace.
164- ns ["_partial_call" ] = functools .partial (result , ** kwgs )
165- result = eval ("_partial_call()" , ns ) # type: ignore # noqa: S307
166- ns .pop ("_partial_call" )
167- while inspect .isawaitable (result ):
168- result = await result
169- ns [name ] = result
170- buffers = ns .pop ("buffers" , [])
171- payload = ns .pop ("payload" , None )
172- if payload is not None :
173- ns ["_call_count" ] = n = ns .get ("_call_count" , 0 ) + 1
174- ns [f"payload_{ n } " ] = payload
175- if namespace_id == "" :
176- self .shell .add_objects_to_ipython_namespace (ns )
177- return {"payload" : payload , "buffers" : buffers }
135+ return await super ()._do_operation_for_frontend (operation , payload , buffers )
178136
179137 def shutdown_kernel (self , vpath : str | None = None ):
180138 "Shutdown the kernel"
@@ -187,8 +145,8 @@ def start_iyplab_python_kernel(self, *, restart=False):
187145 def get_namespace (self , namespace_id = "" , ** objects ) -> LastUpdatedDict :
188146 """Get the namespace corresponding to namespace_id.
189147
190- The namespace is a dictionary that maintains the order by which items
191- are added.
148+ The namespace is a `LastUpdatedDict` that maintains the order by which
149+ items are added.
192150
193151 Default oubjects are added to the namespace via the plugin hook
194152 `default_namespace_objects`.
@@ -201,6 +159,8 @@ def get_namespace(self, namespace_id="", **objects) -> LastUpdatedDict:
201159
202160 Parameters
203161 ----------
162+ namespace_id: str
163+ The identifier for the namespace to use in this kernel.
204164 objects:
205165 Additional objects to add to the namespace.
206166 """
@@ -216,42 +176,135 @@ def get_namespace(self, namespace_id="", **objects) -> LastUpdatedDict:
216176 ns .update (self .comm .kernel .shell .user_ns ) # type: ignore
217177 return ns
218178
179+ async def _evaluate (self , options : dict [str , Any ], buffers : list ):
180+ """Evaluate code for `evaluate`.
181+
182+ A call to this method should originate from a call to `evaluate` from
183+ app in another kernel. The call is sent as a message via the frontend."""
184+ evaluate = options ["evaluate" ]
185+ if isinstance (evaluate , str ):
186+ evaluate = (evaluate ,)
187+ namespace_id = options .get ("namespace_id" , "" )
188+ ns = self .get_namespace (namespace_id , buffers = buffers )
189+ for row in evaluate :
190+ name , expression = ("payload" , row ) if isinstance (row , str ) else row
191+ try :
192+ result = eval (expression , ns ) # noqa: S307
193+ except SyntaxError :
194+ exec (expression , ns ) # noqa: S102
195+ result = next (reversed (ns .values ())) # Requires: LastUpdatedDict
196+ if not name :
197+ continue
198+ while callable (result ) or inspect .isawaitable (result ):
199+ if callable (result ):
200+ kwgs = {}
201+ for p in inspect .signature (result ).parameters :
202+ if p in options :
203+ kwgs [p ] = options [p ]
204+ elif p in ns :
205+ kwgs [p ] = ns [p ]
206+ # We use a partial so that we can evaluate with the same namespace.
207+ ns ["_partial_call" ] = functools .partial (result , ** kwgs )
208+ result = eval ("_partial_call()" , ns ) # type: ignore # noqa: S307
209+ ns .pop ("_partial_call" )
210+ if inspect .isawaitable (result ):
211+ result = await result
212+ if name :
213+ ns [name ] = result
214+ buffers = ns .pop ("buffers" , [])
215+ payload = ns .pop ("payload" , None )
216+ if payload is not None :
217+ ns ["_call_count" ] = n = ns .get ("_call_count" , 0 ) + 1
218+ ns [f"payload_{ n } " ] = payload
219+ if namespace_id == "" :
220+ self .shell .add_objects_to_ipython_namespace (ns )
221+ return {"payload" : payload , "buffers" : buffers }
222+
219223 def evaluate (
220224 self ,
221- evaluate : dict [str , str | inspect ._SourceObjectType ] | str ,
225+ evaluate : str | inspect . _SourceObjectType | Iterable [str | tuple [ str , str | inspect ._SourceObjectType ]] ,
222226 * ,
223227 vpath : str ,
224228 namespace_id = "" ,
229+ kwgs : None | dict = None ,
225230 ** kwargs : Unpack [IpylabKwgs ],
226231 ):
227- """Evaluate code asynchronously in a Python kernel.
232+ """Evaluate code asynchronously in the 'vpath' Python kernel.
233+
234+ Execution is coordinated via the frontend and will evaluate/execute the
235+ code specified. Most forms of expressions are acceptable. Awaitiables
236+ will be awaited recursively prior to sending the result.
228237
229238 Parameters
230239 ----------
231- evaluate: dict[str, str | function | module] | str
232- An expression to evaluate or execute or mapping of values to expressions.
233-
234- The evaluation expression will also be called and or awaited
235- until the returned symbol is no longer callable or awaitable.
236- String:
237- If it is string it will be evaluated and returned.
238- Dict: Advanced usage:
239- A dictionary of `symbol name` to `expression` mappings to be evaluated in the kernel.
240- Each expression is evaluated in turn adding the symbol to the namespace.
241-
242- Expression can be a the name of a function or class. In which case it will be evaluated
243- using parameter names matching the signature of the function or class.
244-
245- ref: https://docs.python.org/3/library/functions.html#eval
246-
247- Once evaluation is complete, the symbols named `payload` and `buffers` will be returned.
240+ evaluate: str | code | Iterable[str | tuple[str|None, str | code]]
241+ An expression or list of expressions to evaluate.
242+
243+ The following combinations are acceptable:
244+ 1. code # Shorthand version -> payload = code
245+ 2. [("payload", code)] -> payload = code
246+ 3. [("payload", code1), ("", code2), code3] -> payload = code3
247+
248+ * Code is handled as a list of mappings of `symbol name` to expressions.
249+ [(symbol name, expression), ...]
250+ * The shorthand version is changed to a single element list automatically.
251+ * `code` is changed to ("payload", code) automatically.
252+ * The latest defined `"payload"` is the return value from evaluation.
253+
254+ Each expression will be evaluated and if a syntax error occurs in evaluation
255+ it will instead be executed. The latest set symbol is taken as the execution
256+ result.
257+
258+ If the result is callable or awaitable it will be called or await recursively
259+ until the result or awaitable is no longer callable or awaitable. To prevent this
260+ make the symbol name an empty string.
261+
262+ References
263+ ----------
264+ * eval: https://docs.python.org/3/library/functions.html#eval
265+ * exec: https://docs.python.org/3/library/functions.html#exec
266+
267+ Once evaluation is complete, the symbols named `payload` and `buffers`
268+ will be returned.
248269 vpath:
249- The path of kernel session where to perform the evaluation.
270+ The path of kernel session where the evaluation should take place .
250271 namespace_id:
251272 The namespace where to perform evaluation.
252- The default namespace will also update the shell.user_ns after successful evaluation.
273+ The default namespace will also update the shell.user_ns after
274+ successful evaluation.
275+ kwgs: dict | None
276+ Specify kwgs that may be used when calling a callable.
277+ Note:The namespace is also searched.
278+
279+ Examples
280+ --------
281+ simple:
282+ ``` python
283+ task = app.evaluate(
284+ "ipylab.app.shell.open_console",
285+ vpath="test",
286+ kwgs={"mode": ipylab.InsertMode.split_right, "activate": False},
287+ )
288+ # The task result will be a ShellConnection. Closing the connection should
289+ # also close the console that was opened.
290+ ```
291+
292+ Advanced example:
293+ ``` python
294+ async def do_something(widget, area):
295+ p = iplab.panel(content=widget)
296+ return p.add_to_shell()
297+
298+
299+ task = app.evaluate(
300+ [("widget", "ipw.Dropdown()"), do_something],
301+ area=iplab.Area.right,
302+ vpath="test",
303+ )
304+ # Task result should be a ShellConnection
305+ ```
253306 """
254- kwgs = {"evaluate" : evaluate , "vpath" : vpath , "namespace_id" : namespace_id }
307+ kwgs = ( kwgs or {}) | {"evaluate" : evaluate , "vpath" : vpath , "namespace_id" : namespace_id }
255308 return self .operation ("evaluate" , kwgs , ** kwargs )
256309
257310
0 commit comments