sier2 0.24__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.24
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.24"
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,7 +1,8 @@
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
6
7
  import sys
7
8
  from typing import Any
@@ -68,7 +69,7 @@ class _BlockContext:
68
69
 
69
70
  def __exit__(self, exc_type, exc_val, exc_tb):
70
71
  if exc_type is None:
71
- 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
72
73
  elif exc_type is KeyboardInterrupt:
73
74
  self.block_state._block_state = BlockState.INTERRUPTED
74
75
  if not self.dag._is_pyodide:
@@ -116,16 +117,48 @@ class _Stopper:
116
117
  def __repr__(self):
117
118
  return f'stopped={self.is_stopped}'
118
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
+
119
144
  class Dag:
120
145
  """A directed acyclic graph of blocks."""
121
146
 
122
- 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):
123
148
  self._block_pairs: list[tuple[Block, Block]] = []
124
-
149
+
125
150
  self.site = site
126
151
  self.title = title
127
152
  self.doc = doc
128
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
+
129
162
  if not self._is_pyodide:
130
163
  self._stopper = _Stopper()
131
164
 
@@ -138,6 +171,10 @@ class Dag:
138
171
  #
139
172
  self._block_context = _BlockContext
140
173
 
174
+ # Set up the logging hook.
175
+ #
176
+ self.logging = _find_logging()
177
+
141
178
  @property
142
179
  def _is_pyodide(self) -> bool:
143
180
  return '_pyodide' in sys.modules
@@ -241,7 +278,7 @@ class Dag:
241
278
  item.values[inp] = new
242
279
  self._block_queue.append(item)
243
280
 
244
- def execute_after_input(self, block: InputBlock, *, dag_logger=None):
281
+ def execute_after_input(self, block: Block, *, dag_logger=None):
245
282
  """Execute the dag after running ``prepare()`` in an input block.
246
283
 
247
284
  After prepare() executes, and the user has possibly
@@ -253,23 +290,23 @@ class Dag:
253
290
 
254
291
  Parameters
255
292
  ----------
256
- block: InputBlock
293
+ block: Block
257
294
  The block to restart the dag at.
258
295
  dag_logger:
259
296
  A logger adapter that will accept log messages.
260
297
  """
261
298
 
262
- if not isinstance(block, InputBlock):
263
- 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}')
264
301
 
265
- # Prime the block queue, using an empty input param values dict
302
+ # Prime the block queue, using _RESTART
266
303
  # to indicate that this is a restart, and Block.execute()
267
304
  # must be called.
268
305
  #
269
- self._block_queue.appendleft(_InputValues(block, {}))
306
+ self._block_queue.appendleft(_InputValues(block, {_RESTART: True}))
270
307
  self.execute(dag_logger=dag_logger)
271
308
 
272
- def execute(self, *, dag_logger=None):
309
+ def execute(self, *, dag_logger=None) -> Block|None:
273
310
  """Execute the dag.
274
311
 
275
312
  The dag is executed by iterating through the block event queue
@@ -277,23 +314,36 @@ class Dag:
277
314
  update the destination block's input parameters and call
278
315
  that block's execute() method.
279
316
 
280
- If the current destination block is an ``InputBlock``,
317
+ If the current destination block's ``block_pause_execution` is True,
281
318
  the loop will call ``block.prepare()` instead of ``block.execute()``,
282
- then stop. The dag can then be restarted with
283
- ``dag.execute_after_input()``.
284
-
285
- To start the dag, there must be something in the event queue -
286
- the dag must be "primed". A block must have updated at least one
287
- output param before the dag's execute() is called. Calling
288
- ``dag.execute()`` will then execute the dag starting with
289
- 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.
290
328
  """
291
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
+
292
340
  if not self._block_queue:
293
341
  # Attempting to execute a dag with no updates is probably a mistake.
294
342
  #
295
343
  raise BlockError('Nothing to execute')
296
344
 
345
+ self.logging(None, sier2_dag_=self)
346
+
297
347
  can_execute = True
298
348
  while self._block_queue:
299
349
  # print(len(self._block_queue), self._block_queue)
@@ -305,6 +355,7 @@ class Dag:
305
355
  can_execute = False
306
356
 
307
357
  item = self._block_queue.popleft()
358
+ is_restart = item.values.pop(_RESTART, False)
308
359
  try:
309
360
  item.dst.param.update(item.values)
310
361
  except ValueError as e:
@@ -318,18 +369,27 @@ class Dag:
318
369
  # unless this is after the user has selected the "Continue"
319
370
  # button.
320
371
  #
321
- is_input_block = isinstance(item.dst, InputBlock)
372
+ is_input_block = item.dst.block_pause_execution
322
373
  if can_execute:
323
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
+
324
381
  # If this is an input block, and there are input
325
382
  # values, call prepare() if it exists.
326
383
  #
327
- if is_input_block and item.values:
328
- g.prepare()
384
+ if is_input_block and not is_restart:# and item.values:
385
+ self.logging(g.prepare, **logging_params)()
329
386
  else:
330
- g.execute()
387
+ self.logging(g.execute, **logging_params)()
331
388
 
332
- 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:
333
393
  # If the current destination block requires user input,
334
394
  # stop executing the dag immediately, because we don't
335
395
  # want to be setting the input params of further blocks
@@ -338,7 +398,9 @@ class Dag:
338
398
  # This possibly leaves items on the queue, which will be
339
399
  # executed on the next call to execute().
340
400
  #
341
- break
401
+ return item.dst
402
+
403
+ return None
342
404
 
343
405
  def disconnect(self, g: Block) -> None:
344
406
  """Disconnect block g from other blocks.
@@ -384,7 +446,7 @@ class Dag:
384
446
 
385
447
  return None
386
448
 
387
- def get_sorted(self):
449
+ def get_sorted(self) -> list[Block]:
388
450
  """Return the blocks in this dag in topological order.
389
451
 
390
452
  This is useful for arranging the blocks in a GUI, for example.
@@ -623,7 +685,7 @@ def _has_cycle(block_pairs: list[tuple[Block, Block]]):
623
685
 
624
686
  return len(remaining)>0
625
687
 
626
- def _get_sorted(block_pairs: list[tuple[Block, Block]]):
688
+ def _get_sorted(block_pairs: list[tuple[Block, Block]]) -> list[Block]:
627
689
  ordered, remaining = topological_sort(block_pairs)
628
690
 
629
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,12 +4,15 @@ 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
12
13
 
14
+ NTHREADS = 2
15
+
13
16
  # From https://tabler.io/icons/icon/info-circle
14
17
  #
15
18
  INFO_SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -25,17 +28,17 @@ if '_pyodide' in sys.modules:
25
28
  # Specifying one thread for panel for some reason tries to start one, so we need to rely on the default.
26
29
  #
27
30
  pn.extension(
28
- 'floatpanel',
29
- inline=True,
30
- loading_spinner='bar',
31
+ 'floatpanel',
32
+ inline=True,
33
+ loading_spinner='bar',
31
34
  notifications=True,
32
35
  )
33
36
  else:
34
37
  pn.extension(
35
- 'floatpanel',
36
- inline=True,
37
- nthreads=2,
38
- loading_spinner='bar',
38
+ 'floatpanel',
39
+ inline=True,
40
+ nthreads=NTHREADS,
41
+ loading_spinner='bar',
39
42
  notifications=True,
40
43
  )
41
44
 
@@ -71,21 +74,19 @@ class _PanelContext:
71
74
  block_logger = getBlockPanelLogger(self.block.name)
72
75
  self.block.logger = block_logger
73
76
 
74
- if self.block._progress:
75
- self.block._progress.active = True
77
+ # if self.block._progress:
78
+ # self.block._progress.active = True
76
79
 
77
80
  return self.block
78
81
 
79
82
  def __exit__(self, exc_type, exc_val, exc_tb):
80
-
81
- print('c')
82
83
  delta = (datetime.now() - self.t0).total_seconds()
83
84
 
84
- if self.block._progress:
85
- self.block._progress.active = False
85
+ # if self.block._progress:
86
+ # self.block._progress.active = False
86
87
 
87
88
  if exc_type is None:
88
- state = BlockState.WAITING if isinstance(self.block, InputBlock) else BlockState.SUCCESSFUL
89
+ state = BlockState.WAITING if self.block.block_pause_execution else BlockState.SUCCESSFUL
89
90
  self.block._block_state = state
90
91
  if self.dag_logger:
91
92
  self.dag_logger.info(f'after {_hms(delta)}', block_name=self.block.name, block_state=state.value)
@@ -136,7 +137,7 @@ def interrupt_thread(tid, exctype):
136
137
  #
137
138
  ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_ulong(tid), None)
138
139
  raise SystemError('PyThreadState_SetAsyncExc failed')
139
-
140
+
140
141
  def _prepare_to_show(dag: Dag):
141
142
  # Replace the default text-based context with the panel-based context.
142
143
  #
@@ -247,6 +248,13 @@ def _show_dag(dag: Dag):
247
248
 
248
249
  pn.state.on_session_destroyed(_quit)
249
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
+
250
258
  template.show(threaded=False)
251
259
 
252
260
  def _serveable_dag(dag: Dag):
@@ -256,8 +264,25 @@ def _serveable_dag(dag: Dag):
256
264
 
257
265
  pn.state.on_session_destroyed(_quit)
258
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
+
259
274
  template.servable()
260
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
+
261
286
  class BlockCard(pn.Card):
262
287
  """A custom card to wrap around a block.
263
288
 
@@ -294,19 +319,22 @@ class BlockCard(pn.Card):
294
319
  # inspect the class and display the param attributes.
295
320
  # This is obviously not what we want.
296
321
  #
297
- # Instead, we want to display an indefinite progress bar.
298
- # The Panel context manager will activate and deactivate it.
322
+ # We just want to display the in_ params.
299
323
  #
300
324
  has_panel = '__panel__' in w.__class__.__dict__
301
325
  if not has_panel:
302
- w._progress = pn.indicators.Progress(
303
- name='Block progress',
304
- bar_color='primary',
305
- active=False,
306
- value=-1
307
- )
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)
308
336
 
309
- if isinstance(w, InputBlock):
337
+ if w.block_pause_execution:
310
338
  # This is an input block, so add a 'Continue' button.
311
339
  #
312
340
  def on_continue(_event):
@@ -318,7 +346,7 @@ class BlockCard(pn.Card):
318
346
  #
319
347
  parent_template.main[0].loading = True
320
348
  w.param.trigger(*w._block_out_params)
321
-
349
+
322
350
  try:
323
351
  if dag_logger:
324
352
  dag_logger.info('', block_name=None, block_state=None)
@@ -343,7 +371,7 @@ class BlockCard(pn.Card):
343
371
  parent_template.main[0].loading = False
344
372
  c_button = pn.widgets.Button(name=w.continue_label, button_type='primary')
345
373
  c_button.on_click(on_continue)
346
-
374
+
347
375
  w_ = pn.Column(
348
376
  w,
349
377
  pn.Row(c_button, align='end'),
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes