sier2 0.23__tar.gz → 0.29__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sier2 might be problematic. Click here for more details.

@@ -1,23 +1,18 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: sier2
3
- Version: 0.23
3
+ Version: 0.29
4
4
  Summary: Blocks of code that are executed in dags
5
- Author: algol60
6
- Author-email: algol60@users.noreply.github.com
7
- Requires-Python: >=3.11,<4.0
8
- Classifier: Intended Audience :: Developers
9
- Classifier: Intended Audience :: Science/Research
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Programming Language :: Python :: 3
5
+ Author: Algol60
6
+ Author-email: algol60 <algol60@users.noreply.github.com>
13
7
  Classifier: Programming Language :: Python :: 3.11
14
8
  Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Programming Language :: Python :: 3.13
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Intended Audience :: Developers
16
13
  Classifier: Topic :: Scientific/Engineering
17
14
  Classifier: Topic :: Software Development :: Libraries
18
- Requires-Dist: holoviews (>=1.19.0)
19
- Requires-Dist: panel (>=1.4.4)
20
- Requires-Dist: param (>=2.1.0)
15
+ Project-URL: Homepage, https://github.com/algol60/sier2
21
16
  Description-Content-Type: text/x-rst
22
17
 
23
18
  Sier2
@@ -1,8 +1,10 @@
1
- [tool.poetry]
1
+ [project]
2
2
  name = "sier2"
3
- version = "0.23"
3
+ version = "0.29"
4
4
  description = "Blocks of code that are executed in dags"
5
- authors = ["algol60 <algol60@users.noreply.github.com>"]
5
+ authors = [
6
+ {name="Algol60", email="algol60 <algol60@users.noreply.github.com>"}
7
+ ]
6
8
  readme = "README.rst"
7
9
  packages = [{include = "sier2", from = "src"}]
8
10
  classifiers = [
@@ -16,7 +18,7 @@ classifiers = [
16
18
  "Topic :: Software Development :: Libraries"
17
19
  ]
18
20
 
19
- [tool.poetry.dependencies]
21
+ [dependencies]
20
22
  python = "^3.11"
21
23
 
22
24
  holoviews = ">=1.19.0"
@@ -26,7 +28,8 @@ param = ">=2.1.0"
26
28
  [[tool.mypy.overrides]]
27
29
  module = [
28
30
  "holoviews",
29
- "param"
31
+ "param",
32
+ "networkx",
30
33
  ]
31
34
  ignore_missing_imports = true
32
35
 
@@ -34,5 +37,5 @@ ignore_missing_imports = true
34
37
  Homepage = "https://github.com/algol60/sier2"
35
38
 
36
39
  [build-system]
37
- requires = ["poetry-core"]
40
+ requires = ["poetry-core>=2.1.1"]
38
41
  build-backend = "poetry.core.masonry.api"
@@ -1,4 +1,4 @@
1
- from ._block import Block, InputBlock, BlockError, BlockValidateError
1
+ from ._block import Block, BlockError, BlockValidateError
2
2
  from ._dag import Connection, Dag, BlockState
3
3
  from ._library import Library, Info
4
4
  from ._version import __version__
@@ -27,6 +27,19 @@ class BlockState(StrEnum):
27
27
  INTERRUPTED = 'INTERRUPTED'
28
28
  ERROR = 'ERROR'
29
29
 
30
+ _PAUSE_EXECUTION_DOC = '''If True, a block executes in two steps.
31
+
32
+ When the block is executed by a dag, the dag first sets the input
33
+ params, then calls ``prepare()``. Execution of the dag then stops.
34
+
35
+ The dag is then restarted using ``dag.execute_after_input(input_block)``.
36
+ (An input block must be specified because it is not required that the
37
+ same input block be used immediately.) This causes the block's
38
+ ``execute()`` method to be called without resetting the input params.
39
+
40
+ Dag execution then continues as normal.
41
+ '''
42
+
30
43
  class Block(param.Parameterized):
31
44
  """The base class for blocks.
32
45
 
@@ -49,16 +62,19 @@ class Block(param.Parameterized):
49
62
  print(f'New value is {self.value_in}')
50
63
  """
51
64
 
65
+ block_pause_execution = param.Boolean(default=False, label='Pause execution', doc=_PAUSE_EXECUTION_DOC)
66
+
52
67
  _block_state = param.String(default=BlockState.READY)
53
68
 
54
69
  SIER2_KEY = '_sier2__key'
55
70
 
56
- def __init__(self, *args, continue_label='Continue', **kwargs):
71
+ def __init__(self, *args, block_pause_execution=False, continue_label='Continue', **kwargs):
57
72
  super().__init__(*args, **kwargs)
58
73
 
59
74
  if not self.__doc__:
60
75
  raise BlockError(f'Class {self.__class__} must have a docstring')
61
76
 
77
+ self.block_pause_execution = block_pause_execution
62
78
  self.continue_label = continue_label
63
79
  # self._block_state = BlockState.READY
64
80
  self.logger = _logger.get_logger(self.name)
@@ -77,7 +93,7 @@ class Block(param.Parameterized):
77
93
 
78
94
  # self._block_context = _EmptyContext()
79
95
 
80
- self._progress = None
96
+ # self._progress = None
81
97
 
82
98
  @classmethod
83
99
  def block_key(cls):
@@ -95,6 +111,17 @@ class Block(param.Parameterized):
95
111
 
96
112
  return f'{im.__name__}.{cls.__qualname__}'
97
113
 
114
+ def prepare(self):
115
+ """If blockpause_execution is True, called by a dag before calling ``execute()```.
116
+
117
+ This gives the block author an opportunity to validate the
118
+ input params and set up a user inteface.
119
+
120
+ After the dag restarts on this block, ``execute()`` will be called.
121
+ """
122
+
123
+ pass
124
+
98
125
  def execute(self, *_, **__):
99
126
  """This method is called when one or more of the input parameters causes an event.
100
127
 
@@ -113,24 +140,24 @@ class Block(param.Parameterized):
113
140
  # print(f'** EXECUTE {self.__class__=}')
114
141
  pass
115
142
 
116
- def __panel__(self):
117
- """A default Panel component.
143
+ # def __panel__(self):
144
+ # """A default Panel component.
118
145
 
119
- When run in a Panel context, a block will typically implement
120
- its own __panel__() method. If it doesn't, this method will be
121
- used as a default. When a block without a __panel__() is wrapped
122
- in a Card, self.progress will be assigned a pn.indicators.Progress()
123
- widget which is returned here. The Panel context will make it active
124
- before executing the block, and non-active after executing the block.
125
- (Why not have a default Progress()? Because we don't want any
126
- Panel-related code in the core implementation.)
146
+ # When run in a Panel context, a block will typically implement
147
+ # its own __panel__() method. If it doesn't, this method will be
148
+ # used as a default. When a block without a __panel__() is wrapped
149
+ # in a Card, self.progress will be assigned a pn.indicators.Progress()
150
+ # widget which is returned here. The Panel context will make it active
151
+ # before executing the block, and non-active after executing the block.
152
+ # (Why not have a default Progress()? Because we don't want any
153
+ # Panel-related code in the core implementation.)
127
154
 
128
- If the block implements __panel__(), this will obviously be overridden.
155
+ # If the block implements __panel__(), this will obviously be overridden.
129
156
 
130
- When run in non-Panel context, this will remain unused.
131
- """
157
+ # When run in non-Panel context, this will remain unused.
158
+ # """
132
159
 
133
- return self._progress
160
+ # return self._progress
134
161
 
135
162
  def __call__(self, **kwargs) -> dict[str, Any]:
136
163
  """Allow a block to be called directly."""
@@ -150,45 +177,45 @@ class Block(param.Parameterized):
150
177
 
151
178
  return result
152
179
 
153
- class InputBlock(Block):
154
- """A ``Block`` that accepts user input.
180
+ # class InputBlock(Block):
181
+ # """A ``Block`` that accepts user input.
155
182
 
156
- An ``InputBlock`` executes in two steps().
183
+ # An ``InputBlock`` executes in two steps().
157
184
 
158
- When the block is executed by a dag, the dag first sets the input
159
- params, then calls ``prepare()``. Execution of the dag then stops.
185
+ # When the block is executed by a dag, the dag first sets the input
186
+ # params, then calls ``prepare()``. Execution of the dag then stops.
160
187
 
161
- The dag is then restarted using ``dag.execute_after_input(input_block)``.
162
- (An input block must be specified because it is not required that the
163
- same input block be used immediately.) This causes the block's
164
- ``execute()`` method to be called without resetting the input params.
188
+ # The dag is then restarted using ``dag.execute_after_input(input_block)``.
189
+ # (An input block must be specified because it is not required that the
190
+ # same input block be used immediately.) This causes the block's
191
+ # ``execute()`` method to be called without resetting the input params.
165
192
 
166
- Dag execution then continues as normal.
167
- """
193
+ # Dag execution then continues as normal.
194
+ # """
168
195
 
169
- def __init__(self, *args, continue_label='Continue', **kwargs):
170
- super().__init__(*args, continue_label=continue_label, **kwargs)
171
- self._block_state = BlockState.INPUT
196
+ # def __init__(self, *args, continue_label='Continue', **kwargs):
197
+ # super().__init__(*args, continue_label=continue_label, **kwargs)
198
+ # self._block_state = BlockState.INPUT
172
199
 
173
- def prepare(self):
174
- """Called by a dag before calling ``execute()```.
200
+ # def prepare(self):
201
+ # """Called by a dag before calling ``execute()```.
175
202
 
176
- This gives the block author an opportunity to validate the
177
- input params and set up a user inteface.
203
+ # This gives the block author an opportunity to validate the
204
+ # input params and set up a user inteface.
178
205
 
179
- After the dag restarts on this block, ``execute()`` will be called.
180
- """
206
+ # After the dag restarts on this block, ``execute()`` will be called.
207
+ # """
181
208
 
182
- pass
209
+ # pass
183
210
 
184
211
  class BlockValidateError(BlockError):
185
- """Raised if ``InputBlock.prepare()`` or ``Block.execute()`` determines that input data is invalid.
212
+ """Raised if ``Block.prepare()`` or ``Block.execute()`` determines that input data is invalid.
186
213
 
187
214
  If this exception is raised, it will be caught by the executing dag.
188
215
  The dag will not set its stop flag, no stacktrace will be displayed,
189
216
  and the error message will be displayed.
190
217
  """
191
218
 
192
- def __init__(self, block_name: str, error: str):
219
+ def __init__(self, *, block_name: str, error: str):
193
220
  super().__init__(error)
194
221
  self.block_name = block_name
@@ -1,8 +1,10 @@
1
- from ._block import Block, InputBlock, BlockError, BlockValidateError, BlockState
1
+ from ._block import Block, BlockError, BlockValidateError, BlockState
2
2
  from dataclasses import dataclass, field #, KW_ONLY, field
3
3
  from collections import defaultdict, deque
4
4
  import holoviews as hv
5
+ from importlib.metadata import entry_points
5
6
  import threading
7
+ import sys
6
8
  from typing import Any
7
9
 
8
10
  # By default, loops in a dag aren't allowed.
@@ -67,10 +69,11 @@ class _BlockContext:
67
69
 
68
70
  def __exit__(self, exc_type, exc_val, exc_tb):
69
71
  if exc_type is None:
70
- self.block._block_state = BlockState.WAITING if isinstance(self.block, InputBlock) else BlockState.SUCCESSFUL
72
+ self.block._block_state = BlockState.WAITING if self.block.block_pause_execution else BlockState.SUCCESSFUL
71
73
  elif exc_type is KeyboardInterrupt:
72
74
  self.block_state._block_state = BlockState.INTERRUPTED
73
- self.dag._stopper.event.set()
75
+ if not self.dag._is_pyodide:
76
+ self.dag._stopper.event.set()
74
77
  print(f'KEYBOARD INTERRUPT IN BLOCK {self.name}')
75
78
  else:
76
79
  state = BlockState.ERROR
@@ -87,7 +90,8 @@ class _BlockContext:
87
90
 
88
91
  # msg = f'While in {self.block.name}.execute(): {exc_val}'
89
92
  # LOGGER.exception(msg)
90
- self.dag._stopper.event.set()
93
+ if not self.dag._is_pyodide:
94
+ self.dag._stopper.event.set()
91
95
 
92
96
  if not issubclass(exc_type, BlockError):
93
97
  # Convert non-BlockErrors in the block to a BlockError.
@@ -113,16 +117,51 @@ class _Stopper:
113
117
  def __repr__(self):
114
118
  return f'stopped={self.is_stopped}'
115
119
 
120
+ def _find_logging():
121
+ PLUGIN_GROUP = 'sier2.logging'
122
+ library = entry_points(group=PLUGIN_GROUP)
123
+ if (liblen:=len(library))==0:
124
+ # There is no logging plugin, so return a dummy.
125
+ #
126
+ return lambda f, *args, **kwargs: f
127
+ elif liblen>1:
128
+ raise BlockError(f'More than one plugin for {PLUGIN_GROUP}')
129
+
130
+ ep = next(iter(library))
131
+ try:
132
+ logging_func = ep.load()
133
+
134
+ return logging_func
135
+ except AttributeError as e:
136
+ e.add_note(f'While attempting to load logging function {ep.value}')
137
+ raise BlockError(e)
138
+
139
+ # A marker from Dag.execute_after_input() to tell Dag.execute()
140
+ # that this a restart.
141
+ #
142
+ _RESTART = ':restart:'
143
+
116
144
  class Dag:
117
145
  """A directed acyclic graph of blocks."""
118
146
 
119
- def __init__(self, *, site: str='Block', title: str, doc: str):
147
+ def __init__(self, *, site: str='Block', title: str, doc: str, author: dict[str, str]=None):
120
148
  self._block_pairs: list[tuple[Block, Block]] = []
121
- self._stopper = _Stopper()
149
+
122
150
  self.site = site
123
151
  self.title = title
124
152
  self.doc = doc
125
153
 
154
+ if author is not None:
155
+ if 'name' in author and 'email' in author:
156
+ self.author = {'name': author['name', 'email': author: 'email']}
157
+ else:
158
+ raise ValueError('Author must contain name and email keys')
159
+ else:
160
+ self.author = None
161
+
162
+ if not self._is_pyodide:
163
+ self._stopper = _Stopper()
164
+
126
165
  # We watch output params to be notified when they are set.
127
166
  # Events are queued here.
128
167
  #
@@ -132,6 +171,14 @@ class Dag:
132
171
  #
133
172
  self._block_context = _BlockContext
134
173
 
174
+ # Set up the logging hook.
175
+ #
176
+ self.logging = _find_logging()
177
+
178
+ @property
179
+ def _is_pyodide(self) -> bool:
180
+ return '_pyodide' in sys.modules
181
+
135
182
  def _for_each_once(self):
136
183
  """Yield each connected block once."""
137
184
 
@@ -144,13 +191,13 @@ class Dag:
144
191
 
145
192
  def stop(self):
146
193
  """Stop further execution of Block instances in this dag."""
147
-
148
- self._stopper.event.set()
194
+ if not self._is_pyodide:
195
+ self._stopper.event.set()
149
196
 
150
197
  def unstop(self):
151
198
  """Enable further execution of Block instances in this dag."""
152
-
153
- self._stopper.event.clear()
199
+ if not self._is_pyodide:
200
+ self._stopper.event.clear()
154
201
 
155
202
  def connect(self, src: Block, dst: Block, *connections: Connection):
156
203
  if any(not isinstance(c, Connection) for c in connections):
@@ -231,7 +278,7 @@ class Dag:
231
278
  item.values[inp] = new
232
279
  self._block_queue.append(item)
233
280
 
234
- def execute_after_input(self, block: InputBlock, *, dag_logger=None):
281
+ def execute_after_input(self, block: Block, *, dag_logger=None):
235
282
  """Execute the dag after running ``prepare()`` in an input block.
236
283
 
237
284
  After prepare() executes, and the user has possibly
@@ -243,23 +290,23 @@ class Dag:
243
290
 
244
291
  Parameters
245
292
  ----------
246
- block: InputBlock
293
+ block: Block
247
294
  The block to restart the dag at.
248
295
  dag_logger:
249
296
  A logger adapter that will accept log messages.
250
297
  """
251
298
 
252
- if not isinstance(block, InputBlock):
253
- raise BlockError(f'A dag can only restart an InputBlock, not {block.name}')
299
+ if not block.block_pause_execution:
300
+ raise BlockError(f'A dag can only restart a paused Block, not {block.name}')
254
301
 
255
- # Prime the block queue, using an empty input param values dict
302
+ # Prime the block queue, using _RESTART
256
303
  # to indicate that this is a restart, and Block.execute()
257
304
  # must be called.
258
305
  #
259
- self._block_queue.appendleft(_InputValues(block, {}))
306
+ self._block_queue.appendleft(_InputValues(block, {_RESTART: True}))
260
307
  self.execute(dag_logger=dag_logger)
261
308
 
262
- def execute(self, *, dag_logger=None):
309
+ def execute(self, *, dag_logger=None) -> Block|None:
263
310
  """Execute the dag.
264
311
 
265
312
  The dag is executed by iterating through the block event queue
@@ -267,38 +314,54 @@ class Dag:
267
314
  update the destination block's input parameters and call
268
315
  that block's execute() method.
269
316
 
270
- If the current destination block is an ``InputBlock``,
317
+ If the current destination block's ``block_pause_execution` is True,
271
318
  the loop will call ``block.prepare()` instead of ``block.execute()``,
272
- then stop. The dag can then be restarted with
273
- ``dag.execute_after_input()``.
274
-
275
- To start the dag, there must be something in the event queue -
276
- the dag must be "primed". A block must have updated at least one
277
- output param before the dag's execute() is called. Calling
278
- ``dag.execute()`` will then execute the dag starting with
279
- the blocks connected to the first block.
319
+ then stop; execute() will return the block that is puased on.
320
+ The dag can then be restarted with ``dag.execute_after_input()``,
321
+ using the paused block as the parameter.
322
+
323
+ To start the dag, either:
324
+ - there must be something in the event queue - the dag must be "primed". A block must have updated at least one output param before the dag's execute() is called;
325
+ - the first block in the dag must be an input block (block_pause_execution=True).
326
+
327
+ Calling ``dag.execute()`` will then execute the dag starting with the relevant block.
280
328
  """
281
329
 
330
+ if not self._block_queue:
331
+ # If there aren't any blocks on the queue, find the first block in the dag.
332
+ # If this block is an input block, put it on the queue.
333
+ #
334
+ sorted_blocks = self.get_sorted()
335
+ if sorted_blocks:
336
+ first = sorted_blocks[0]
337
+ if first.block_pause_execution:
338
+ self._block_queue.appendleft(_InputValues(first, {}))
339
+
282
340
  if not self._block_queue:
283
341
  # Attempting to execute a dag with no updates is probably a mistake.
284
342
  #
285
343
  raise BlockError('Nothing to execute')
286
344
 
345
+ self.logging(None, sier2_dag_=self)
346
+
287
347
  can_execute = True
288
348
  while self._block_queue:
289
349
  # print(len(self._block_queue), self._block_queue)
290
350
  # The user has set the "stop executing" flag.
291
351
  # Continue to set params, but don't execute anything
292
352
  #
293
- if self._stopper.is_stopped:
294
- can_execute = False
353
+ if not self._is_pyodide:
354
+ if self._stopper.is_stopped:
355
+ can_execute = False
295
356
 
296
357
  item = self._block_queue.popleft()
358
+ is_restart = item.values.pop(_RESTART, False)
297
359
  try:
298
360
  item.dst.param.update(item.values)
299
361
  except ValueError as e:
300
362
  msg = f'While in {item.dst.name} setting a parameter: {e}'
301
- self._stopper.event.set()
363
+ if not self._is_pyodide:
364
+ self._stopper.event.set()
302
365
  raise BlockError(msg) from e
303
366
 
304
367
  # Execute the block.
@@ -306,18 +369,27 @@ class Dag:
306
369
  # unless this is after the user has selected the "Continue"
307
370
  # button.
308
371
  #
309
- is_input_block = isinstance(item.dst, InputBlock)
372
+ is_input_block = item.dst.block_pause_execution
310
373
  if can_execute:
311
374
  with self._block_context(block=item.dst, dag=self, dag_logger=dag_logger) as g:
375
+
376
+ logging_params = {
377
+ 'sier2_dag_': self,
378
+ 'sier2_block_': f'{item.dst}'
379
+ }
380
+
312
381
  # If this is an input block, and there are input
313
382
  # values, call prepare() if it exists.
314
383
  #
315
- if is_input_block and item.values:
316
- g.prepare()
384
+ if is_input_block and not is_restart:# and item.values:
385
+ self.logging(g.prepare, **logging_params)()
317
386
  else:
318
- g.execute()
387
+ self.logging(g.execute, **logging_params)()
319
388
 
320
- if is_input_block and item.values:
389
+ # print(f'{is_input_block=}')
390
+ # print(f'{is_restart=}')
391
+ # print(f'{item.values=}')
392
+ if is_input_block and not is_restart:# and item.values:
321
393
  # If the current destination block requires user input,
322
394
  # stop executing the dag immediately, because we don't
323
395
  # want to be setting the input params of further blocks
@@ -326,7 +398,9 @@ class Dag:
326
398
  # This possibly leaves items on the queue, which will be
327
399
  # executed on the next call to execute().
328
400
  #
329
- break
401
+ return item.dst
402
+
403
+ return None
330
404
 
331
405
  def disconnect(self, g: Block) -> None:
332
406
  """Disconnect block g from other blocks.
@@ -372,7 +446,7 @@ class Dag:
372
446
 
373
447
  return None
374
448
 
375
- def get_sorted(self):
449
+ def get_sorted(self) -> list[Block]:
376
450
  """Return the blocks in this dag in topological order.
377
451
 
378
452
  This is useful for arranging the blocks in a GUI, for example.
@@ -611,7 +685,7 @@ def _has_cycle(block_pairs: list[tuple[Block, Block]]):
611
685
 
612
686
  return len(remaining)>0
613
687
 
614
- def _get_sorted(block_pairs: list[tuple[Block, Block]]):
688
+ def _get_sorted(block_pairs: list[tuple[Block, Block]]) -> list[Block]:
615
689
  ordered, remaining = topological_sort(block_pairs)
616
690
 
617
691
  if remaining:
@@ -24,6 +24,29 @@ def docstring(func) -> str:
24
24
 
25
25
  return doc.split('\n')[0].strip()
26
26
 
27
+ def _import_item(key):
28
+ """Look up an object by key.
29
+
30
+ The returned object may be a class (if a Block key) or a function (if a dag key).
31
+
32
+ See the Entry points specification at
33
+ https://packaging.python.org/en/latest/specifications/entry-points/#entry-points.
34
+ """
35
+
36
+ modname, qualname_separator, qualname = key.partition(':')
37
+ try:
38
+ obj = importlib.import_module(modname)
39
+ if qualname_separator:
40
+ for attr in qualname.split('.'):
41
+ obj = getattr(obj, attr)
42
+
43
+ return obj
44
+ except ModuleNotFoundError as e:
45
+ msg = str(e)
46
+ if not qualname_separator:
47
+ msg = f'{msg}. Is there a \':\' missing?'
48
+ raise BlockError(msg)
49
+
27
50
  def _find_blocks():
28
51
  yield from _find('blocks')
29
52
 
@@ -143,8 +166,6 @@ class Library:
143
166
  if not issubclass(block_class, Block):
144
167
  print(f'{key} is not a Block')
145
168
 
146
- # if not key:
147
- # key = block_class.block_key()
148
169
  key_ = key if key else block_class.block_key()
149
170
 
150
171
  if key_ in _block_library:
@@ -161,9 +182,7 @@ class Library:
161
182
  raise BlockError(f'Block name {key} is not in the library')
162
183
 
163
184
  if _block_library[key] is None:
164
- ix = key.rfind('.')
165
- m = importlib.import_module(key[:ix])
166
- cls = getattr(m, key[ix+1:])
185
+ cls = _import_item(key)
167
186
  if not issubclass(cls, Block):
168
187
  raise BlockError(f'{key} is not a block')
169
188
 
@@ -188,9 +207,7 @@ class Library:
188
207
  raise BlockError(f'Dag name {key} is not in the library')
189
208
 
190
209
  if key in _dag_library:
191
- ix = key.rfind('.')
192
- m = importlib.import_module(key[:ix])
193
- func = getattr(m, key[ix+1:])
210
+ func = _import_item(key)
194
211
  dag = func()
195
212
  if not isinstance(dag, Dag):
196
213
  raise BlockError(f'{key} is not a dag')
@@ -4,8 +4,9 @@ import html
4
4
  import panel as pn
5
5
  import sys
6
6
  import threading
7
+ from typing import Callable
7
8
 
8
- from sier2 import Block, InputBlock, BlockValidateError, BlockState, Dag, BlockError
9
+ from sier2 import Block, BlockValidateError, BlockState, Dag, BlockError
9
10
  from .._dag import _InputValues
10
11
  from ._feedlogger import getDagPanelLogger, getBlockPanelLogger
11
12
  from ._panel_util import _get_state_color, dag_doc
@@ -22,7 +23,26 @@ INFO_SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" vie
22
23
  </svg>
23
24
  '''
24
25
 
25
- pn.extension('floatpanel', inline=True, nthreads=NTHREADS, loading_spinner='bar', notifications=True)
26
+ if '_pyodide' in sys.modules:
27
+ # Pyodide (to be specific, WASM) doesn't allow threads.
28
+ # Specifying one thread for panel for some reason tries to start one, so we need to rely on the default.
29
+ #
30
+ pn.extension(
31
+ 'floatpanel',
32
+ inline=True,
33
+ loading_spinner='bar',
34
+ notifications=True,
35
+ )
36
+ else:
37
+ pn.extension(
38
+ 'floatpanel',
39
+ inline=True,
40
+ nthreads=NTHREADS,
41
+ loading_spinner='bar',
42
+ notifications=True,
43
+ )
44
+
45
+
26
46
 
27
47
  def _hms(sec):
28
48
  h, sec = divmod(int(sec), 3600)
@@ -54,26 +74,27 @@ class _PanelContext:
54
74
  block_logger = getBlockPanelLogger(self.block.name)
55
75
  self.block.logger = block_logger
56
76
 
57
- if self.block._progress:
58
- self.block._progress.active = True
77
+ # if self.block._progress:
78
+ # self.block._progress.active = True
59
79
 
60
80
  return self.block
61
81
 
62
82
  def __exit__(self, exc_type, exc_val, exc_tb):
63
83
  delta = (datetime.now() - self.t0).total_seconds()
64
84
 
65
- if self.block._progress:
66
- self.block._progress.active = False
85
+ # if self.block._progress:
86
+ # self.block._progress.active = False
67
87
 
68
88
  if exc_type is None:
69
- state = BlockState.WAITING if isinstance(self.block, InputBlock) else BlockState.SUCCESSFUL
89
+ state = BlockState.WAITING if self.block.block_pause_execution else BlockState.SUCCESSFUL
70
90
  self.block._block_state = state
71
91
  if self.dag_logger:
72
92
  self.dag_logger.info(f'after {_hms(delta)}', block_name=self.block.name, block_state=state.value)
73
93
  elif isinstance(exc_type, KeyboardInterrupt):
74
94
  state = BlockState.INTERRUPTED
75
95
  self.block_state._block_state = state
76
- self.dag._stopper.event.set()
96
+ if not self.dag._is_pyodide:
97
+ self.dag._stopper.event.set()
77
98
  if self.dag_logger:
78
99
  self.dag_logger.exception(f'KEYBOARD INTERRUPT after {_hms(delta)}', block_name=self.block.name, block_state=state)
79
100
  else:
@@ -88,13 +109,13 @@ class _PanelContext:
88
109
  )
89
110
 
90
111
  # msg = f'While in {self.block.name}.execute(): {exc_val}'
91
- self.dag._stopper.event.set()
112
+ if not self.dag._is_pyodide:
113
+ self.dag._stopper.event.set()
92
114
 
93
115
  if not issubclass(exc_type, BlockError):
94
116
  # Convert the error in the block to a BlockError.
95
117
  #
96
118
  raise BlockError(f'Block {self.block.name}: {str(exc_val)}') from exc_val
97
-
98
119
  return False
99
120
 
100
121
  def _quit(session_context):
@@ -116,7 +137,7 @@ def interrupt_thread(tid, exctype):
116
137
  #
117
138
  ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), None)
118
139
  raise SystemError('PyThreadState_SetAsyncExc failed')
119
-
140
+
120
141
  def _prepare_to_show(dag: Dag):
121
142
  # Replace the default text-based context with the panel-based context.
122
143
  #
@@ -227,6 +248,13 @@ def _show_dag(dag: Dag):
227
248
 
228
249
  pn.state.on_session_destroyed(_quit)
229
250
 
251
+ # Execute the dag.
252
+ # Since this is a panel dag, we expect the first block to be an input nlock.
253
+ # This ensures that the first block's prepare() method is called.
254
+ # If the first block is not an input block, it must be primed, just like a plain dag.
255
+ #
256
+ dag.execute()
257
+
230
258
  template.show(threaded=False)
231
259
 
232
260
  def _serveable_dag(dag: Dag):
@@ -236,8 +264,25 @@ def _serveable_dag(dag: Dag):
236
264
 
237
265
  pn.state.on_session_destroyed(_quit)
238
266
 
267
+ # Execute the dag.
268
+ # Since this is a panel dag, we expect the first block to be an input nlock.
269
+ # This ensures that the first block's prepare() method is called.
270
+ # If the first block is not an input block, it must be primed, just like a plain dag.
271
+ #
272
+ dag.execute()
273
+
239
274
  template.servable()
240
275
 
276
+ def _default_panel(self) -> Callable[[Block], pn.Param]:
277
+ """Provide a default __panel__() implementation for blocks that don't have one.
278
+
279
+ This default will display the in_ parameters.
280
+ """
281
+
282
+ in_names = [name for name in self.param.values() if name.startswith('in_')]
283
+
284
+ return pn.Param(self, parameters=in_names, show_name=False)
285
+
241
286
  class BlockCard(pn.Card):
242
287
  """A custom card to wrap around a block.
243
288
 
@@ -274,19 +319,22 @@ class BlockCard(pn.Card):
274
319
  # inspect the class and display the param attributes.
275
320
  # This is obviously not what we want.
276
321
  #
277
- # Instead, we want to display an indefinite progress bar.
278
- # The Panel context manager will activate and deactivate it.
322
+ # We just want to display the in_ params.
279
323
  #
280
324
  has_panel = '__panel__' in w.__class__.__dict__
281
325
  if not has_panel:
282
- w._progress = pn.indicators.Progress(
283
- name='Block progress',
284
- bar_color='primary',
285
- active=False,
286
- value=-1
287
- )
326
+ # w._progress = pn.indicators.Progress(
327
+ # name='Block progress',
328
+ # bar_color='primary',
329
+ # active=False,
330
+ # value=-1
331
+ # )
332
+
333
+ # Go go gadget descriptor protocol.
334
+ #
335
+ w.__panel__ = _default_panel.__get__(w)
288
336
 
289
- if isinstance(w, InputBlock):
337
+ if w.block_pause_execution:
290
338
  # This is an input block, so add a 'Continue' button.
291
339
  #
292
340
  def on_continue(_event):
@@ -296,8 +344,9 @@ class BlockCard(pn.Card):
296
344
  # current values on the queue.
297
345
  # If their values are already there, it doesn't matter.
298
346
  #
299
- w.param.trigger(*w._block_out_params)
300
347
  parent_template.main[0].loading = True
348
+ w.param.trigger(*w._block_out_params)
349
+
301
350
  try:
302
351
  if dag_logger:
303
352
  dag_logger.info('', block_name=None, block_state=None)
@@ -320,7 +369,6 @@ class BlockCard(pn.Card):
320
369
  pn.state.notifications.error(notif, duration=0)
321
370
  finally:
322
371
  parent_template.main[0].loading = False
323
-
324
372
  c_button = pn.widgets.Button(name=w.continue_label, button_type='primary')
325
373
  c_button.on_click(on_continue)
326
374
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes