cellgraph 0.2.0__tar.gz → 0.3.0__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.
- {cellgraph-0.2.0 → cellgraph-0.3.0}/PKG-INFO +1 -1
- {cellgraph-0.2.0 → cellgraph-0.3.0}/pyproject.toml +1 -1
- {cellgraph-0.2.0 → cellgraph-0.3.0}/pyproject.toml.orig +1 -1
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/__init__.py +2 -0
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/analysis.py +55 -2
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/core.py +39 -2
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/effects.py +6 -1
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/keys.py +39 -6
- {cellgraph-0.2.0 → cellgraph-0.3.0}/LICENSE +0 -0
- {cellgraph-0.2.0 → cellgraph-0.3.0}/README.md +0 -0
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/diagnostics.py +0 -0
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/grammar.py +0 -0
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/languages.py +0 -0
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/plan.py +0 -0
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/py.typed +0 -0
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/python.py +0 -0
- {cellgraph-0.2.0 → cellgraph-0.3.0}/src/cellgraph/vocabulary.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cellgraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The cell graph of a notebook or script: annotations with a host-supplied vocabulary, analysis, the DAG, and the keys that say whether a cell must run.
|
|
5
5
|
Author: Fangchen Li
|
|
6
6
|
Author-email: Fangchen Li <fangchen.li@outlook.com>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cellgraph"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "The cell graph of a notebook or script: annotations with a host-supplied vocabulary, analysis, the DAG, and the keys that say whether a cell must run."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cellgraph"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
4
4
|
description = "The cell graph of a notebook or script: annotations with a host-supplied vocabulary, analysis, the DAG, and the keys that say whether a cell must run."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -40,6 +40,7 @@ from cellgraph.grammar import GRAMMAR, Directive, Grammar
|
|
|
40
40
|
from cellgraph.keys import (
|
|
41
41
|
PARTS_SCHEMA,
|
|
42
42
|
HostKeyed,
|
|
43
|
+
Item,
|
|
43
44
|
Keyed,
|
|
44
45
|
OutputRef,
|
|
45
46
|
Provenance,
|
|
@@ -105,6 +106,7 @@ __all__ = [
|
|
|
105
106
|
"GroupConfig",
|
|
106
107
|
"HostKeyed",
|
|
107
108
|
"Input",
|
|
109
|
+
"Item",
|
|
108
110
|
"Keeps",
|
|
109
111
|
"Keyed",
|
|
110
112
|
"LanguageAnalyzer",
|
|
@@ -136,6 +136,8 @@ class Dag:
|
|
|
136
136
|
inactive_cells: set[str] = field(default_factory=set)
|
|
137
137
|
wired_inputs: dict[str, dict[str, set[str]]] = field(default_factory=dict)
|
|
138
138
|
fanout_cells: dict[str, SweepProducer] = field(default_factory=dict)
|
|
139
|
+
# An ``@each`` cell: the name its items come from. Not a sweep: a reader sees one output.
|
|
140
|
+
each_cells: dict[str, str] = field(default_factory=dict)
|
|
139
141
|
read_producers: dict[str, dict[str, str | SweepProducer]] = field(default_factory=dict)
|
|
140
142
|
|
|
141
143
|
def wired_variables(self, consumer_id: str, producer_id: str) -> set[str]:
|
|
@@ -511,6 +513,20 @@ def _names_phase(ca: CellAnalysis, analyzed: Analyzed) -> None:
|
|
|
511
513
|
args={"name": name},
|
|
512
514
|
)
|
|
513
515
|
|
|
516
|
+
for effect in ca.effects:
|
|
517
|
+
if isinstance(effect, FanOut) and effect.over is not None:
|
|
518
|
+
# An instance's key leaves the source out, so the body must not read it whole.
|
|
519
|
+
if effect.over in analyzed.references:
|
|
520
|
+
ca.error(
|
|
521
|
+
"each_reads_source",
|
|
522
|
+
f"the cell reads `{effect.over}` itself, which every item's result would "
|
|
523
|
+
"then depend on; read each item, or compute it in a cell before this one",
|
|
524
|
+
annotation="each",
|
|
525
|
+
args={"source": effect.over},
|
|
526
|
+
)
|
|
527
|
+
elif effect.over not in bound and effect.over not in references:
|
|
528
|
+
references.append(effect.over)
|
|
529
|
+
|
|
514
530
|
ca.defines = defines
|
|
515
531
|
ca.references = [name for name in references if name not in bound]
|
|
516
532
|
ca.builtin_references = [name for name in analyzed.builtin_references if name not in bound]
|
|
@@ -555,7 +571,7 @@ def _names_phase(ca: CellAnalysis, analyzed: Analyzed) -> None:
|
|
|
555
571
|
if ca.fanout is not None and ca.fanout != effect:
|
|
556
572
|
ca.error(
|
|
557
573
|
"fanout_conflict",
|
|
558
|
-
"the cell fans out
|
|
574
|
+
"the cell fans out two ways",
|
|
559
575
|
annotation=origin,
|
|
560
576
|
line=origin_line,
|
|
561
577
|
)
|
|
@@ -823,7 +839,10 @@ def _wiring_phase(cells: list[CellAnalysis], groups: GroupConfig) -> Dag:
|
|
|
823
839
|
|
|
824
840
|
is_sweep_member = cell.id in sweep_member_group
|
|
825
841
|
fanout_group: str | None = None
|
|
826
|
-
if cell.fanout is not None:
|
|
842
|
+
if cell.fanout is not None and cell.fanout.over is not None:
|
|
843
|
+
dag.each_cells[cell.id] = cell.fanout.over
|
|
844
|
+
_each_notes(cell, dag)
|
|
845
|
+
elif cell.fanout is not None:
|
|
827
846
|
read_groups = dag.sweep_groups_read(cell.id)
|
|
828
847
|
named = cell.fanout.group
|
|
829
848
|
if named is not None:
|
|
@@ -872,6 +891,39 @@ def _wiring_phase(cells: list[CellAnalysis], groups: GroupConfig) -> Dag:
|
|
|
872
891
|
return dag
|
|
873
892
|
|
|
874
893
|
|
|
894
|
+
def _each_notes(cell: CellAnalysis, dag: Dag) -> None:
|
|
895
|
+
"""What an ``@each`` cell cannot iterate, or be."""
|
|
896
|
+
assert cell.fanout is not None and cell.fanout.over is not None
|
|
897
|
+
source = cell.fanout.over
|
|
898
|
+
line = _first_line(cell.directives, "each")
|
|
899
|
+
if cell.member is not None:
|
|
900
|
+
cell.error(
|
|
901
|
+
"each_on_variant_member",
|
|
902
|
+
"a variant member cannot also run once per item",
|
|
903
|
+
annotation="each",
|
|
904
|
+
line=line,
|
|
905
|
+
)
|
|
906
|
+
if source in cell.bound:
|
|
907
|
+
return
|
|
908
|
+
producer = dag.read_producer(cell.id, source)
|
|
909
|
+
if isinstance(producer, SweepProducer):
|
|
910
|
+
cell.error(
|
|
911
|
+
"each_over_sweep",
|
|
912
|
+
f"`{source}` comes from a sweep, which has no one collection to iterate",
|
|
913
|
+
annotation="each",
|
|
914
|
+
line=line,
|
|
915
|
+
args={"source": source},
|
|
916
|
+
)
|
|
917
|
+
elif producer is None:
|
|
918
|
+
cell.error(
|
|
919
|
+
"each_unknown_source",
|
|
920
|
+
f"no cell before this one defines `{source}`, and the cell does not bind it",
|
|
921
|
+
annotation="each",
|
|
922
|
+
line=line,
|
|
923
|
+
args={"source": source},
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
|
|
875
927
|
def _fanout_notes(
|
|
876
928
|
cell: CellAnalysis, read_groups: set[str], sizes: Mapping[str, tuple[str, ...]]
|
|
877
929
|
) -> None:
|
|
@@ -950,6 +1002,7 @@ def _restrict(graph: Dag, keep: set[str]) -> Dag:
|
|
|
950
1002
|
if c in keep
|
|
951
1003
|
},
|
|
952
1004
|
fanout_cells={c: p for c, p in graph.fanout_cells.items() if c in keep},
|
|
1005
|
+
each_cells={c: s for c, s in graph.each_cells.items() if c in keep},
|
|
953
1006
|
read_producers={c: dict(r) for c, r in graph.read_producers.items() if c in keep},
|
|
954
1007
|
)
|
|
955
1008
|
active = [cid for cid in ids if cid not in graph.inactive_cells]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""The core vocabulary: ``@name``, ``@after``, ``@nocache``, ``@variant``, ``@per_variant
|
|
1
|
+
"""The core vocabulary: ``@name``, ``@after``, ``@nocache``, ``@variant``, ``@per_variant``,
|
|
2
|
+
``@each``.
|
|
2
3
|
|
|
3
4
|
The parsing rules and diagnostic codes are Strata's (``annotations.py`` and
|
|
4
5
|
``annotation_validation.py``), which these were ported from.
|
|
@@ -6,7 +7,7 @@ The parsing rules and diagnostic codes are Strata's (``annotations.py`` and
|
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
|
-
from cellgraph.effects import After, FanOut, Member, NoCache
|
|
10
|
+
from cellgraph.effects import After, Binds, FanOut, Member, NoCache
|
|
10
11
|
from cellgraph.grammar import Directive
|
|
11
12
|
from cellgraph.vocabulary import Annotation, AnnotationError, flag, mark_stacked, scalar
|
|
12
13
|
|
|
@@ -56,6 +57,36 @@ def _parse_per_variant(directives: list[Directive]) -> str | None:
|
|
|
56
57
|
return tokens[0] if tokens else None
|
|
57
58
|
|
|
58
59
|
|
|
60
|
+
def _parse_each(directives: list[Directive]) -> dict[str, str]:
|
|
61
|
+
"""``# @each <name> in <source> [key=<column>] [glob=<pattern>]``; the last line wins."""
|
|
62
|
+
value: dict[str, str] = {}
|
|
63
|
+
for directive in directives:
|
|
64
|
+
tokens = directive.value.split()
|
|
65
|
+
value = {}
|
|
66
|
+
if len(tokens) >= 3 and tokens[1] == "in":
|
|
67
|
+
value = {"name": tokens[0], "source": tokens[2]}
|
|
68
|
+
for token in tokens[3:]:
|
|
69
|
+
option, _, setting = token.partition("=")
|
|
70
|
+
if option not in ("key", "glob") or not setting or option in value:
|
|
71
|
+
value = {}
|
|
72
|
+
break
|
|
73
|
+
value[option] = setting
|
|
74
|
+
if not value or not (value["name"].isidentifier() and value["source"].isidentifier()):
|
|
75
|
+
raise AnnotationError(
|
|
76
|
+
f"`@each {directive.value}`: expected `@each <name> in <source> "
|
|
77
|
+
"[key=<column>] [glob=<pattern>]`, with the name and source Python identifiers",
|
|
78
|
+
code="each_malformed",
|
|
79
|
+
line=directive.line,
|
|
80
|
+
)
|
|
81
|
+
if value["name"] == value["source"]:
|
|
82
|
+
raise AnnotationError(
|
|
83
|
+
f"`@each {directive.value}`: an item cannot have its source's name",
|
|
84
|
+
code="each_malformed",
|
|
85
|
+
line=directive.line,
|
|
86
|
+
)
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
59
90
|
def core_annotations() -> list[Annotation]:
|
|
60
91
|
return [
|
|
61
92
|
Annotation("name", parse=scalar(str), in_key=False),
|
|
@@ -84,4 +115,10 @@ def core_annotations() -> list[Annotation]:
|
|
|
84
115
|
parse=_parse_per_variant,
|
|
85
116
|
effects=lambda group: [FanOut(group)],
|
|
86
117
|
),
|
|
118
|
+
# What it iterates, and how an item is named, decide what each instance is.
|
|
119
|
+
Annotation(
|
|
120
|
+
"each",
|
|
121
|
+
parse=_parse_each,
|
|
122
|
+
effects=lambda each: [Binds(each["name"]), FanOut(over=each["source"])],
|
|
123
|
+
),
|
|
87
124
|
]
|
|
@@ -88,9 +88,14 @@ class Member:
|
|
|
88
88
|
|
|
89
89
|
@dataclass(frozen=True)
|
|
90
90
|
class FanOut:
|
|
91
|
-
"""Run once per member of ``group``, or of the single sweep group the cell reads.
|
|
91
|
+
"""Run once per member of ``group``, or of the single sweep group the cell reads.
|
|
92
|
+
|
|
93
|
+
With ``over``, once per item the host lists from that name instead: an upstream
|
|
94
|
+
variable, or a name the cell binds. The cell reads it through its items alone.
|
|
95
|
+
"""
|
|
92
96
|
|
|
93
97
|
group: str | None = None
|
|
98
|
+
over: str | None = None
|
|
94
99
|
|
|
95
100
|
|
|
96
101
|
@dataclass(frozen=True)
|
|
@@ -104,6 +104,18 @@ class Published:
|
|
|
104
104
|
cell_ref: OutputRef | Unavailable | None = None
|
|
105
105
|
|
|
106
106
|
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class Item:
|
|
109
|
+
"""One item of an ``@each`` cell: what the host calls it, and its current content.
|
|
110
|
+
|
|
111
|
+
The key is the instance's name, so it must be stable across runs; the digest is
|
|
112
|
+
what makes the instance run again.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
key: str
|
|
116
|
+
digest: str
|
|
117
|
+
|
|
118
|
+
|
|
107
119
|
@dataclass(frozen=True)
|
|
108
120
|
class Resolved:
|
|
109
121
|
"""A resolver's answer for one input.
|
|
@@ -277,6 +289,7 @@ def _upstream(
|
|
|
277
289
|
cell_id: str,
|
|
278
290
|
instance: str | None,
|
|
279
291
|
published: Callable[[str], Published],
|
|
292
|
+
without: str | None = None,
|
|
280
293
|
) -> _Assembly:
|
|
281
294
|
graph = analysis.graph
|
|
282
295
|
cell = analysis.cells[cell_id]
|
|
@@ -307,6 +320,8 @@ def _upstream(
|
|
|
307
320
|
for var in names:
|
|
308
321
|
if by_content and var not in reads:
|
|
309
322
|
continue
|
|
323
|
+
if var == without:
|
|
324
|
+
continue
|
|
310
325
|
read = variant_read(analysis, cell_id, upstream_id, var, instance)
|
|
311
326
|
if read is not None and read.fanout:
|
|
312
327
|
parts = []
|
|
@@ -356,7 +371,7 @@ def execution_key(
|
|
|
356
371
|
analysis: Analysis,
|
|
357
372
|
cell_id: str,
|
|
358
373
|
*,
|
|
359
|
-
instance: str | None = None,
|
|
374
|
+
instance: str | Item | None = None,
|
|
360
375
|
published: Callable[[str], Published],
|
|
361
376
|
resolve: Mapping[str, Resolver] | None = None,
|
|
362
377
|
env_hash: str,
|
|
@@ -364,7 +379,9 @@ def execution_key(
|
|
|
364
379
|
"""The key that says whether *cell_id* (or one fan-out *instance*) must run.
|
|
365
380
|
|
|
366
381
|
``published(cell_id)`` is what an upstream published; ``resolve`` has one resolver
|
|
367
|
-
per input kind. ``instance=None`` on a fan-out cell is its cell-level key.
|
|
382
|
+
per input kind. ``instance=None`` on a fan-out cell is its cell-level key. An ``@each``
|
|
383
|
+
cell's instance is an :class:`Item`, keyed on that item in place of its source, so
|
|
384
|
+
changing one item leaves every other item's key alone.
|
|
368
385
|
"""
|
|
369
386
|
from cellgraph.analysis import STRUCTURAL_CODES, NotPlannableError
|
|
370
387
|
|
|
@@ -381,18 +398,32 @@ def execution_key(
|
|
|
381
398
|
raise NotPlannableError(cell_id, structural)
|
|
382
399
|
owns_key = analysis.analyzers[cell.language].owns_key
|
|
383
400
|
fanout = analysis.graph.fanout_cells.get(cell_id)
|
|
384
|
-
|
|
401
|
+
source = analysis.graph.each_cells.get(cell_id)
|
|
402
|
+
item = instance if isinstance(instance, Item) else None
|
|
403
|
+
if item is not None:
|
|
404
|
+
if source is None:
|
|
405
|
+
raise ValueError(f"cell {cell_id} has no items, so it has no item {item.key!r}")
|
|
406
|
+
instance = item.key
|
|
407
|
+
elif instance is not None:
|
|
408
|
+
if source is not None:
|
|
409
|
+
raise ValueError(f"cell {cell_id} runs per item; {instance!r} is not an Item")
|
|
385
410
|
if fanout is None:
|
|
386
411
|
raise ValueError(f"cell {cell_id} does not fan out, so it has no instance {instance!r}")
|
|
387
412
|
if instance not in {name for name, _ in fanout.variants}:
|
|
388
413
|
raise ValueError(f"cell {cell_id} has no instance {instance!r}")
|
|
389
414
|
|
|
390
415
|
legacy = analysis.digest == "legacy-1"
|
|
391
|
-
|
|
416
|
+
if item is not None:
|
|
417
|
+
assembly = _upstream(analysis, cell_id, None, published, without=source)
|
|
418
|
+
assembly.add(f"each:{source}", f"each:{item.key}={item.digest}")
|
|
419
|
+
else:
|
|
420
|
+
assembly = _upstream(analysis, cell_id, instance, published)
|
|
392
421
|
inputs: list[tuple[str, str, str]] = []
|
|
393
422
|
resolvers = resolve or {}
|
|
394
|
-
# A host-keyed cell's inputs are the host's to key.
|
|
423
|
+
# A host-keyed cell's inputs are the host's to key. An item stands in for its source.
|
|
395
424
|
for spec in cell.inputs if owns_key else []:
|
|
425
|
+
if item is not None and spec.name == source:
|
|
426
|
+
continue
|
|
396
427
|
if spec.kind not in resolvers:
|
|
397
428
|
raise ValueError(f"no resolver for {spec.kind!r} inputs (cell {cell_id})")
|
|
398
429
|
result = resolvers[spec.kind](spec)
|
|
@@ -434,7 +465,9 @@ def execution_key(
|
|
|
434
465
|
components = [value for _, value in assembly.components] + [c for _, _, c in inputs]
|
|
435
466
|
if legacy:
|
|
436
467
|
digest = compute_provenance_hash(components, cell.definition_key, env_hash)
|
|
437
|
-
if
|
|
468
|
+
if item is not None:
|
|
469
|
+
digest = derive_subkey(digest, f"each={item.key}")
|
|
470
|
+
elif instance is not None:
|
|
438
471
|
digest = derive_subkey(digest, f"variant={instance}")
|
|
439
472
|
else:
|
|
440
473
|
digest = _sha256(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|