cellgraph 0.1.0__tar.gz → 0.2.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.1.0 → cellgraph-0.2.0}/PKG-INFO +1 -1
- {cellgraph-0.1.0 → cellgraph-0.2.0}/pyproject.toml +1 -1
- {cellgraph-0.1.0 → cellgraph-0.2.0}/pyproject.toml.orig +1 -1
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/__init__.py +4 -0
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/analysis.py +128 -26
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/diagnostics.py +9 -2
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/keys.py +66 -33
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/python.py +18 -1
- {cellgraph-0.1.0 → cellgraph-0.2.0}/LICENSE +0 -0
- {cellgraph-0.1.0 → cellgraph-0.2.0}/README.md +0 -0
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/core.py +0 -0
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/effects.py +0 -0
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/grammar.py +0 -0
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/languages.py +0 -0
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/plan.py +0 -0
- {cellgraph-0.1.0 → cellgraph-0.2.0}/src/cellgraph/py.typed +0 -0
- {cellgraph-0.1.0 → cellgraph-0.2.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.2.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.2.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.2.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"
|
|
@@ -45,8 +45,10 @@ from cellgraph.keys import (
|
|
|
45
45
|
Provenance,
|
|
46
46
|
Published,
|
|
47
47
|
Resolved,
|
|
48
|
+
Resolver,
|
|
48
49
|
Run,
|
|
49
50
|
Unavailable,
|
|
51
|
+
VariantRead,
|
|
50
52
|
canonical_json,
|
|
51
53
|
compute_provenance_hash,
|
|
52
54
|
derive_subkey,
|
|
@@ -117,11 +119,13 @@ __all__ = [
|
|
|
117
119
|
"PythonAnalyzer",
|
|
118
120
|
"Reads",
|
|
119
121
|
"Resolved",
|
|
122
|
+
"Resolver",
|
|
120
123
|
"Run",
|
|
121
124
|
"Seeds",
|
|
122
125
|
"Severity",
|
|
123
126
|
"SweepProducer",
|
|
124
127
|
"Unavailable",
|
|
128
|
+
"VariantRead",
|
|
125
129
|
"UnknownLanguageError",
|
|
126
130
|
"ValidationContext",
|
|
127
131
|
"VariantGroupResolution",
|
|
@@ -275,6 +275,8 @@ class CellAnalysis:
|
|
|
275
275
|
annotations: dict[str, Any] = field(default_factory=dict)
|
|
276
276
|
directives: list[Directive] = field(default_factory=list)
|
|
277
277
|
effects: list[Effect] = field(default_factory=list)
|
|
278
|
+
# Per effect: the annotation key and line it came from; None for the host's hook.
|
|
279
|
+
origins: list[tuple[str | None, int | None]] = field(default_factory=list)
|
|
278
280
|
defines: list[str] = field(default_factory=list)
|
|
279
281
|
references: list[str] = field(default_factory=list)
|
|
280
282
|
builtin_references: list[str] = field(default_factory=list)
|
|
@@ -282,6 +284,7 @@ class CellAnalysis:
|
|
|
282
284
|
bound: list[str] = field(default_factory=list)
|
|
283
285
|
after: list[str] = field(default_factory=list)
|
|
284
286
|
seeds: dict[str, str] = field(default_factory=dict)
|
|
287
|
+
seed_origins: dict[str, tuple[str | None, int | None]] = field(default_factory=dict)
|
|
285
288
|
member: Member | None = None
|
|
286
289
|
fanout: FanOut | None = None
|
|
287
290
|
kept: list[str] = field(default_factory=list)
|
|
@@ -292,12 +295,21 @@ class CellAnalysis:
|
|
|
292
295
|
plannable: bool = True
|
|
293
296
|
# The cells whose own errors keep this one from planning (itself included).
|
|
294
297
|
blocked_by: list[str] = field(default_factory=list)
|
|
298
|
+
# Set by ``analyze`` for every cell, plannable or not.
|
|
295
299
|
definition_key: str | None = None
|
|
296
300
|
normalized: str | None = None
|
|
297
301
|
|
|
298
|
-
def error(
|
|
302
|
+
def error(
|
|
303
|
+
self,
|
|
304
|
+
code: str,
|
|
305
|
+
message: str,
|
|
306
|
+
*,
|
|
307
|
+
annotation: str | None = None,
|
|
308
|
+
line=None,
|
|
309
|
+
args: Mapping[str, Any] | None = None,
|
|
310
|
+
):
|
|
299
311
|
self.diagnostics.append(
|
|
300
|
-
Diagnostic(code, Severity.ERROR, message, self.id, annotation, line)
|
|
312
|
+
Diagnostic(code, Severity.ERROR, message, self.id, annotation, line, dict(args or {}))
|
|
301
313
|
)
|
|
302
314
|
|
|
303
315
|
def note(
|
|
@@ -308,8 +320,11 @@ class CellAnalysis:
|
|
|
308
320
|
*,
|
|
309
321
|
annotation: str | None = None,
|
|
310
322
|
line=None,
|
|
323
|
+
args: Mapping[str, Any] | None = None,
|
|
311
324
|
):
|
|
312
|
-
self.diagnostics.append(
|
|
325
|
+
self.diagnostics.append(
|
|
326
|
+
Diagnostic(code, severity, message, self.id, annotation, line, dict(args or {}))
|
|
327
|
+
)
|
|
313
328
|
|
|
314
329
|
@property
|
|
315
330
|
def has_error(self) -> bool:
|
|
@@ -354,6 +369,14 @@ def _first_line(directives: list[Directive], key: str) -> int | None:
|
|
|
354
369
|
return next((d.line for d in directives if d.key == key), None)
|
|
355
370
|
|
|
356
371
|
|
|
372
|
+
def _naming_line(directives: list[Directive], key: str, token: str) -> int | None:
|
|
373
|
+
"""The first *key* line with *token* among its values; else the first *key* line."""
|
|
374
|
+
for d in directives:
|
|
375
|
+
if d.key == key and token in (t.rstrip(",") for t in d.value.split()):
|
|
376
|
+
return d.line
|
|
377
|
+
return _first_line(directives, key)
|
|
378
|
+
|
|
379
|
+
|
|
357
380
|
def _top_level_imports(source: str) -> set[str]:
|
|
358
381
|
try:
|
|
359
382
|
tree = ast.parse(source)
|
|
@@ -389,6 +412,7 @@ def _parse_phase(
|
|
|
389
412
|
f"`@{directive.key}` is not an annotation this vocabulary knows.{hint}",
|
|
390
413
|
annotation=directive.key,
|
|
391
414
|
line=directive.line,
|
|
415
|
+
args={"key": directive.key, "suggestion": close[0] if close else None},
|
|
392
416
|
)
|
|
393
417
|
continue
|
|
394
418
|
by_owner.setdefault(owner.key, []).append(directive)
|
|
@@ -427,14 +451,19 @@ def _parse_phase(
|
|
|
427
451
|
f"({type(encoded).__name__}); the annotation needs an encoder",
|
|
428
452
|
annotation=annotation.key,
|
|
429
453
|
line=directives[0].line,
|
|
454
|
+
args={"type": type(encoded).__name__},
|
|
430
455
|
)
|
|
431
456
|
else:
|
|
432
457
|
ca.key_fields[annotation.key] = encoded
|
|
433
458
|
if annotation.effects is not None:
|
|
434
|
-
|
|
459
|
+
produced = list(annotation.effects(value))
|
|
460
|
+
ca.effects.extend(produced)
|
|
461
|
+
ca.origins.extend([(annotation.key, directives[0].line)] * len(produced))
|
|
435
462
|
|
|
436
463
|
if vocabulary.cell_effects is not None:
|
|
437
|
-
|
|
464
|
+
hooked = list(vocabulary.cell_effects(cell.meta, dict(ca.annotations)))
|
|
465
|
+
ca.effects.extend(hooked)
|
|
466
|
+
ca.origins.extend([(None, None)] * len(hooked))
|
|
438
467
|
for effect in ca.effects:
|
|
439
468
|
if not isinstance(effect, EFFECT_TYPES):
|
|
440
469
|
raise TypeError(f"{effect!r} is not one of the ten effects")
|
|
@@ -472,11 +501,15 @@ def _names_phase(ca: CellAnalysis, analyzed: Analyzed) -> None:
|
|
|
472
501
|
if isinstance(effect, Binds):
|
|
473
502
|
for name in effect.names:
|
|
474
503
|
if name in bound:
|
|
475
|
-
ca.error("bind_conflict", f"`{name}` is bound twice")
|
|
504
|
+
ca.error("bind_conflict", f"`{name}` is bound twice", args={"name": name})
|
|
476
505
|
else:
|
|
477
506
|
bound.append(name)
|
|
478
507
|
for name in sorted(set(bound) & set(read)):
|
|
479
|
-
ca.error(
|
|
508
|
+
ca.error(
|
|
509
|
+
"bind_read_conflict",
|
|
510
|
+
f"`{name}` is both bound and read from upstream",
|
|
511
|
+
args={"name": name},
|
|
512
|
+
)
|
|
480
513
|
|
|
481
514
|
ca.defines = defines
|
|
482
515
|
ca.references = [name for name in references if name not in bound]
|
|
@@ -484,32 +517,59 @@ def _names_phase(ca: CellAnalysis, analyzed: Analyzed) -> None:
|
|
|
484
517
|
ca.mutation_defines = list(analyzed.mutation_defines)
|
|
485
518
|
ca.bound = bound
|
|
486
519
|
|
|
487
|
-
for effect in ca.effects:
|
|
520
|
+
for effect, (origin, origin_line) in zip(ca.effects, ca.origins, strict=True):
|
|
488
521
|
if isinstance(effect, After):
|
|
489
522
|
for cell_id in effect.cell_ids:
|
|
490
523
|
if cell_id not in ca.after:
|
|
491
524
|
ca.after.append(cell_id)
|
|
492
525
|
elif isinstance(effect, Seeds):
|
|
493
526
|
if effect.name in ca.seeds and ca.seeds[effect.name] != effect.cell_id:
|
|
494
|
-
ca.error(
|
|
527
|
+
ca.error(
|
|
528
|
+
"seed_conflict",
|
|
529
|
+
f"`{effect.name}` is seeded from two cells",
|
|
530
|
+
annotation=origin,
|
|
531
|
+
line=origin_line,
|
|
532
|
+
args={"name": effect.name},
|
|
533
|
+
)
|
|
495
534
|
else:
|
|
496
535
|
ca.seeds[effect.name] = effect.cell_id
|
|
536
|
+
ca.seed_origins[effect.name] = (origin, origin_line)
|
|
497
537
|
if effect.name in bound:
|
|
498
|
-
ca.error(
|
|
538
|
+
ca.error(
|
|
539
|
+
"bind_read_conflict",
|
|
540
|
+
f"`{effect.name}` is both bound and seeded",
|
|
541
|
+
annotation=origin,
|
|
542
|
+
line=origin_line,
|
|
543
|
+
args={"name": effect.name},
|
|
544
|
+
)
|
|
499
545
|
elif isinstance(effect, Member):
|
|
500
546
|
if ca.member is not None and ca.member != effect:
|
|
501
|
-
ca.error(
|
|
547
|
+
ca.error(
|
|
548
|
+
"member_conflict",
|
|
549
|
+
"the cell is a member of two variants",
|
|
550
|
+
annotation=origin,
|
|
551
|
+
line=origin_line,
|
|
552
|
+
)
|
|
502
553
|
ca.member = effect
|
|
503
554
|
elif isinstance(effect, FanOut):
|
|
504
555
|
if ca.fanout is not None and ca.fanout != effect:
|
|
505
|
-
ca.error(
|
|
556
|
+
ca.error(
|
|
557
|
+
"fanout_conflict",
|
|
558
|
+
"the cell fans out over two groups",
|
|
559
|
+
annotation=origin,
|
|
560
|
+
line=origin_line,
|
|
561
|
+
)
|
|
506
562
|
ca.fanout = effect
|
|
507
563
|
elif isinstance(effect, Keeps):
|
|
508
564
|
ca.kept.extend(name for name in effect.names if name not in ca.kept)
|
|
509
565
|
elif isinstance(effect, Input):
|
|
510
566
|
if any((i.kind, i.name) == (effect.kind, effect.name) for i in ca.inputs):
|
|
511
567
|
ca.error(
|
|
512
|
-
"input_conflict",
|
|
568
|
+
"input_conflict",
|
|
569
|
+
f"two {effect.kind} inputs have the address {effect.name!r}",
|
|
570
|
+
annotation=origin,
|
|
571
|
+
line=origin_line,
|
|
572
|
+
args={"kind": effect.kind, "name": effect.name},
|
|
513
573
|
)
|
|
514
574
|
else:
|
|
515
575
|
ca.inputs.append(effect)
|
|
@@ -537,7 +597,12 @@ def _resolve_groups(
|
|
|
537
597
|
f"{cell.id[:8]} in group '{group}'"
|
|
538
598
|
)
|
|
539
599
|
for cid in (clash, cell.id):
|
|
540
|
-
by_id[cid].error(
|
|
600
|
+
by_id[cid].error(
|
|
601
|
+
"variant_collision",
|
|
602
|
+
message,
|
|
603
|
+
annotation="variant",
|
|
604
|
+
args={"group": group, "name": name, "cells": [clash, cell.id]},
|
|
605
|
+
)
|
|
541
606
|
continue
|
|
542
607
|
if not members:
|
|
543
608
|
group_order.append(group)
|
|
@@ -591,6 +656,13 @@ def _variant_notes(cells: list[CellAnalysis], dag: Dag, groups: GroupConfig) ->
|
|
|
591
656
|
f"defines {sorted(theirs)}",
|
|
592
657
|
annotation="variant",
|
|
593
658
|
line=line,
|
|
659
|
+
args={
|
|
660
|
+
"group": group,
|
|
661
|
+
"name": cell.member.name,
|
|
662
|
+
"defines": sorted(own),
|
|
663
|
+
"sibling": sibling.member.name if sibling.member else None,
|
|
664
|
+
"sibling_defines": sorted(theirs),
|
|
665
|
+
},
|
|
594
666
|
)
|
|
595
667
|
break
|
|
596
668
|
mode = groups.modes.get(group, "switch")
|
|
@@ -601,6 +673,7 @@ def _variant_notes(cells: list[CellAnalysis], dag: Dag, groups: GroupConfig) ->
|
|
|
601
673
|
f"group `{group}` has mode {mode!r}; it runs as a switch",
|
|
602
674
|
annotation="variant",
|
|
603
675
|
line=line,
|
|
676
|
+
args={"group": group, "mode": mode},
|
|
604
677
|
)
|
|
605
678
|
active = groups.active.get(group)
|
|
606
679
|
if mode == "sweep" and active:
|
|
@@ -610,6 +683,7 @@ def _variant_notes(cells: list[CellAnalysis], dag: Dag, groups: GroupConfig) ->
|
|
|
610
683
|
f"group `{group}` is a sweep, so its active selection is ignored",
|
|
611
684
|
annotation="variant",
|
|
612
685
|
line=line,
|
|
686
|
+
args={"group": group, "active": active},
|
|
613
687
|
)
|
|
614
688
|
if mode != "sweep" and active:
|
|
615
689
|
names = [name for _, name in members[group].members]
|
|
@@ -620,6 +694,7 @@ def _variant_notes(cells: list[CellAnalysis], dag: Dag, groups: GroupConfig) ->
|
|
|
620
694
|
f"group `{group}` selects `{active}`, which no cell provides",
|
|
621
695
|
annotation="variant",
|
|
622
696
|
line=line,
|
|
697
|
+
args={"group": group, "active": active},
|
|
623
698
|
)
|
|
624
699
|
|
|
625
700
|
|
|
@@ -683,27 +758,35 @@ def _wiring_phase(cells: list[CellAnalysis], groups: GroupConfig) -> Dag:
|
|
|
683
758
|
|
|
684
759
|
for name, seed_id in cell.seeds.items():
|
|
685
760
|
seed = cell_by_id.get(seed_id)
|
|
761
|
+
origin, origin_line = cell.seed_origins.get(name, (None, None))
|
|
686
762
|
if seed is None:
|
|
687
|
-
|
|
763
|
+
reason, message = "unknown", f"`{name}` is seeded from {seed_id!r}, no cell"
|
|
688
764
|
elif seed_id == cell.id:
|
|
689
|
-
|
|
765
|
+
reason, message = "self", f"`{name}` is seeded from the cell itself"
|
|
690
766
|
elif seed_id in inactive:
|
|
691
|
-
|
|
692
|
-
"
|
|
767
|
+
reason, message = (
|
|
768
|
+
"inactive",
|
|
769
|
+
f"`{name}` is seeded from {seed_id}, an inactive variant",
|
|
693
770
|
)
|
|
694
771
|
elif name not in seed.defines:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
f"`{name}` is seeded from {seed_id}, which does not define it",
|
|
698
|
-
)
|
|
772
|
+
reason = "undefined"
|
|
773
|
+
message = f"`{name}` is seeded from {seed_id}, which does not define it"
|
|
699
774
|
else:
|
|
700
775
|
dag.read_producers.setdefault(cell.id, {})[name] = seed_id
|
|
701
776
|
_wire(dag, seed_id, cell.id, name)
|
|
777
|
+
continue
|
|
778
|
+
cell.error(
|
|
779
|
+
"seed_unknown_cell",
|
|
780
|
+
message,
|
|
781
|
+
annotation=origin,
|
|
782
|
+
line=origin_line,
|
|
783
|
+
args={"name": name, "cell_id": seed_id, "reason": reason},
|
|
784
|
+
)
|
|
702
785
|
|
|
703
786
|
dag.consumed_variables[cell.id].update(v for v in cell.kept if v in cell.defines)
|
|
704
787
|
|
|
705
788
|
for upstream_id in cell.after:
|
|
706
|
-
line =
|
|
789
|
+
line = _naming_line(cell.directives, "after", upstream_id)
|
|
707
790
|
if upstream_id == cell.id:
|
|
708
791
|
cell.note(
|
|
709
792
|
"after_self",
|
|
@@ -711,6 +794,7 @@ def _wiring_phase(cells: list[CellAnalysis], groups: GroupConfig) -> Dag:
|
|
|
711
794
|
"`@after` names this cell",
|
|
712
795
|
annotation="after",
|
|
713
796
|
line=line,
|
|
797
|
+
args={"cell_id": upstream_id},
|
|
714
798
|
)
|
|
715
799
|
continue
|
|
716
800
|
if upstream_id not in cell_by_id:
|
|
@@ -719,6 +803,7 @@ def _wiring_phase(cells: list[CellAnalysis], groups: GroupConfig) -> Dag:
|
|
|
719
803
|
f"`@after {upstream_id}` names no cell",
|
|
720
804
|
annotation="after",
|
|
721
805
|
line=line,
|
|
806
|
+
args={"cell_id": upstream_id},
|
|
722
807
|
)
|
|
723
808
|
continue
|
|
724
809
|
if upstream_id not in active_ids:
|
|
@@ -727,6 +812,7 @@ def _wiring_phase(cells: list[CellAnalysis], groups: GroupConfig) -> Dag:
|
|
|
727
812
|
f"`@after {upstream_id}` names a variant that is not active",
|
|
728
813
|
annotation="after",
|
|
729
814
|
line=line,
|
|
815
|
+
args={"cell_id": upstream_id},
|
|
730
816
|
)
|
|
731
817
|
continue
|
|
732
818
|
dag.edges.append(DagEdge(from_cell_id=upstream_id, to_cell_id=cell.id, variable=""))
|
|
@@ -781,7 +867,7 @@ def _wiring_phase(cells: list[CellAnalysis], groups: GroupConfig) -> Dag:
|
|
|
781
867
|
message = "Cycle detected in DAG: " + (" → ".join(cycles[0]) if cycles else "unknown")
|
|
782
868
|
for cid in active_cell_ids:
|
|
783
869
|
if cid in on_cycle:
|
|
784
|
-
cell_by_id[cid].error("cycle", message)
|
|
870
|
+
cell_by_id[cid].error("cycle", message, args={"cycle": cycles[0] if cycles else []})
|
|
785
871
|
dag.topological_order = _kahn(dag, active_cell_ids)
|
|
786
872
|
return dag
|
|
787
873
|
|
|
@@ -810,6 +896,7 @@ def _fanout_notes(
|
|
|
810
896
|
f"`@per_variant {named}` names no sweep group the cell reads",
|
|
811
897
|
annotation="per_variant",
|
|
812
898
|
line=line,
|
|
899
|
+
args={"group": named},
|
|
813
900
|
)
|
|
814
901
|
return
|
|
815
902
|
effective = named
|
|
@@ -830,6 +917,7 @@ def _fanout_notes(
|
|
|
830
917
|
f"`@per_variant` reads {', '.join(sorted(read_groups))}; name one",
|
|
831
918
|
annotation="per_variant",
|
|
832
919
|
line=line,
|
|
920
|
+
args={"groups": sorted(read_groups)},
|
|
833
921
|
)
|
|
834
922
|
return
|
|
835
923
|
effective = next(iter(read_groups))
|
|
@@ -840,6 +928,7 @@ def _fanout_notes(
|
|
|
840
928
|
f"group `{effective}` has one variant",
|
|
841
929
|
annotation="per_variant",
|
|
842
930
|
line=line,
|
|
931
|
+
args={"group": effective},
|
|
843
932
|
)
|
|
844
933
|
|
|
845
934
|
|
|
@@ -909,6 +998,20 @@ class Analysis:
|
|
|
909
998
|
|
|
910
999
|
return execution_key(self, cell_id, **kwargs)
|
|
911
1000
|
|
|
1001
|
+
def variant_read(
|
|
1002
|
+
self, cell_id: str, upstream_id: str, var: str, instance: str | None = None
|
|
1003
|
+
) -> Any:
|
|
1004
|
+
"""See ``cellgraph.keys.variant_read``."""
|
|
1005
|
+
from cellgraph.keys import variant_read
|
|
1006
|
+
|
|
1007
|
+
return variant_read(self, cell_id, upstream_id, var, instance)
|
|
1008
|
+
|
|
1009
|
+
def shadowed_reads(self, cell_id: str) -> set[str]:
|
|
1010
|
+
"""See ``cellgraph.keys.shadowed_reads``."""
|
|
1011
|
+
from cellgraph.keys import shadowed_reads
|
|
1012
|
+
|
|
1013
|
+
return shadowed_reads(self, cell_id)
|
|
1014
|
+
|
|
912
1015
|
def plan(self, target: str, ready: Callable[[str], bool]) -> list[str]:
|
|
913
1016
|
"""See ``cellgraph.plan.plan``."""
|
|
914
1017
|
from cellgraph.plan import plan
|
|
@@ -1008,6 +1111,5 @@ def analyze(
|
|
|
1008
1111
|
digest=digest,
|
|
1009
1112
|
)
|
|
1010
1113
|
for ca in analyses:
|
|
1011
|
-
|
|
1012
|
-
ca.normalized, ca.definition_key = definition_key(analysis, ca)
|
|
1114
|
+
ca.normalized, ca.definition_key = definition_key(analysis, ca)
|
|
1013
1115
|
return analysis
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass, field
|
|
6
7
|
from enum import StrEnum
|
|
8
|
+
from typing import Any
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class Severity(StrEnum):
|
|
@@ -16,7 +18,11 @@ class Severity(StrEnum):
|
|
|
16
18
|
|
|
17
19
|
@dataclass(frozen=True)
|
|
18
20
|
class Diagnostic:
|
|
19
|
-
"""One finding. ``annotation`` is the directive key it is about, if any.
|
|
21
|
+
"""One finding. ``annotation`` is the directive key it is about, if any.
|
|
22
|
+
|
|
23
|
+
``args`` holds what the message was built from, such as the cell id an ``@after``
|
|
24
|
+
names, so a host can word the finding its own way.
|
|
25
|
+
"""
|
|
20
26
|
|
|
21
27
|
code: str
|
|
22
28
|
severity: Severity
|
|
@@ -24,3 +30,4 @@ class Diagnostic:
|
|
|
24
30
|
cell: str | None = None
|
|
25
31
|
annotation: str | None = None
|
|
26
32
|
line: int | None = None
|
|
33
|
+
args: Mapping[str, Any] = field(default_factory=dict, compare=False)
|
|
@@ -142,7 +142,11 @@ class Unavailable:
|
|
|
142
142
|
|
|
143
143
|
@dataclass(frozen=True)
|
|
144
144
|
class HostKeyed:
|
|
145
|
-
"""The host computes this cell's key (``owns_key=False``).
|
|
145
|
+
"""The host computes this cell's key (``owns_key=False``).
|
|
146
|
+
|
|
147
|
+
``parts`` holds the upstream components the cell reads, as a key would, and no
|
|
148
|
+
inputs: those are the host's to key.
|
|
149
|
+
"""
|
|
146
150
|
|
|
147
151
|
parts: dict[str, Any] | None = None
|
|
148
152
|
|
|
@@ -174,15 +178,23 @@ def definition_key(analysis: Analysis, cell: CellAnalysis) -> tuple[str, str]:
|
|
|
174
178
|
|
|
175
179
|
|
|
176
180
|
@dataclass(frozen=True)
|
|
177
|
-
class
|
|
181
|
+
class VariantRead:
|
|
182
|
+
"""What a cell reads of one variable a sweep makes: the variants, and how.
|
|
183
|
+
|
|
184
|
+
``scalar``: a fan-out instance's own group, read as that variant's value; otherwise
|
|
185
|
+
the ``{variant: value}`` dict. ``fanout``: the variants are one fan-out cell's
|
|
186
|
+
instances rather than the members of a group. No variants: another variant's
|
|
187
|
+
member, which this cell does not read.
|
|
188
|
+
"""
|
|
189
|
+
|
|
178
190
|
variants: tuple[str, ...]
|
|
179
191
|
scalar: bool
|
|
180
192
|
fanout: bool
|
|
181
193
|
|
|
182
194
|
|
|
183
|
-
def
|
|
184
|
-
analysis: Analysis, cell_id: str, upstream_id: str, var: str, instance: str | None
|
|
185
|
-
) ->
|
|
195
|
+
def variant_read(
|
|
196
|
+
analysis: Analysis, cell_id: str, upstream_id: str, var: str, instance: str | None = None
|
|
197
|
+
) -> VariantRead | None:
|
|
186
198
|
"""What *cell_id* (or its *instance*) reads of *var* from *upstream_id*, if a sweep makes it."""
|
|
187
199
|
from cellgraph.analysis import SweepProducer
|
|
188
200
|
|
|
@@ -198,15 +210,33 @@ def _variant_read(
|
|
|
198
210
|
names = (
|
|
199
211
|
(instance,) if own and instance is not None else tuple(n for n, _ in producer.variants)
|
|
200
212
|
)
|
|
201
|
-
return
|
|
213
|
+
return VariantRead(variants=names, scalar=own, fanout=True)
|
|
202
214
|
member = next((name for name, cid in producer.variants if cid == upstream_id), None)
|
|
203
215
|
if member is None:
|
|
204
216
|
return None
|
|
205
217
|
if own:
|
|
206
|
-
return
|
|
218
|
+
return VariantRead(
|
|
207
219
|
variants=(member,) if member == instance else (), scalar=True, fanout=False
|
|
208
220
|
)
|
|
209
|
-
return
|
|
221
|
+
return VariantRead(variants=(member,), scalar=False, fanout=False)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def shadowed_reads(analysis: Analysis, cell_id: str) -> set[str]:
|
|
225
|
+
"""Names *cell_id* reads that an upstream defines but is not wired to give it.
|
|
226
|
+
|
|
227
|
+
Such a name is read from its wired producer only.
|
|
228
|
+
"""
|
|
229
|
+
graph = analysis.graph
|
|
230
|
+
cell = analysis.cells[cell_id]
|
|
231
|
+
reads = set(cell.references) | set(cell.builtin_references)
|
|
232
|
+
shadowed: set[str] = set()
|
|
233
|
+
for upstream_id in graph.cell_upstream.get(cell_id, []):
|
|
234
|
+
upstream = analysis.cells.get(upstream_id)
|
|
235
|
+
if upstream is not None:
|
|
236
|
+
shadowed |= (reads & set(upstream.defines)) - graph.wired_variables(
|
|
237
|
+
cell_id, upstream_id
|
|
238
|
+
)
|
|
239
|
+
return shadowed
|
|
210
240
|
|
|
211
241
|
|
|
212
242
|
class _Assembly:
|
|
@@ -256,14 +286,7 @@ def _upstream(
|
|
|
256
286
|
return assembly
|
|
257
287
|
|
|
258
288
|
reads = set(cell.references) | set(cell.builtin_references)
|
|
259
|
-
|
|
260
|
-
shadowed: set[str] = set()
|
|
261
|
-
for upstream_id in upstream_ids:
|
|
262
|
-
upstream = analysis.cells.get(upstream_id)
|
|
263
|
-
if upstream is not None:
|
|
264
|
-
shadowed |= (reads & set(upstream.defines)) - graph.wired_variables(
|
|
265
|
-
cell_id, upstream_id
|
|
266
|
-
)
|
|
289
|
+
shadowed = shadowed_reads(analysis, cell_id)
|
|
267
290
|
|
|
268
291
|
sweep_buckets: dict[str, list[tuple[str, str]]] = {}
|
|
269
292
|
for upstream_id in upstream_ids:
|
|
@@ -274,19 +297,17 @@ def _upstream(
|
|
|
274
297
|
wired = graph.wired_variables(cell_id, upstream_id) if shadowed else set()
|
|
275
298
|
snapshot = published(upstream_id)
|
|
276
299
|
|
|
277
|
-
|
|
278
|
-
if not
|
|
279
|
-
|
|
300
|
+
names = list(snapshot.outputs)
|
|
301
|
+
if not names and snapshot.cell_ref is not None:
|
|
302
|
+
value = assembly.component(snapshot.cell_ref, by_content, f"cell:{upstream_id}")
|
|
303
|
+
if value is not None:
|
|
304
|
+
assembly.add(f"cell:{upstream_id}", value)
|
|
280
305
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
value = assembly.component(ref, by_content, f"cell:{upstream_id}")
|
|
284
|
-
if value is not None:
|
|
285
|
-
assembly.add(f"cell:{upstream_id}", value)
|
|
286
|
-
continue
|
|
306
|
+
# An output is looked up only once it counts, so a host's mapping can be lazy.
|
|
307
|
+
for var in names:
|
|
287
308
|
if by_content and var not in reads:
|
|
288
309
|
continue
|
|
289
|
-
read =
|
|
310
|
+
read = variant_read(analysis, cell_id, upstream_id, var, instance)
|
|
290
311
|
if read is not None and read.fanout:
|
|
291
312
|
parts = []
|
|
292
313
|
for name in read.variants:
|
|
@@ -304,7 +325,9 @@ def _upstream(
|
|
|
304
325
|
continue
|
|
305
326
|
if read is not None and not read.variants:
|
|
306
327
|
continue # another variant's member
|
|
307
|
-
value = assembly.component(
|
|
328
|
+
value = assembly.component(
|
|
329
|
+
snapshot.outputs[var], by_content, f"output:{upstream_id}.{var}"
|
|
330
|
+
)
|
|
308
331
|
if value is None:
|
|
309
332
|
continue
|
|
310
333
|
if read is not None:
|
|
@@ -343,13 +366,20 @@ def execution_key(
|
|
|
343
366
|
``published(cell_id)`` is what an upstream published; ``resolve`` has one resolver
|
|
344
367
|
per input kind. ``instance=None`` on a fan-out cell is its cell-level key.
|
|
345
368
|
"""
|
|
346
|
-
from cellgraph.analysis import NotPlannableError
|
|
369
|
+
from cellgraph.analysis import STRUCTURAL_CODES, NotPlannableError
|
|
347
370
|
|
|
348
371
|
cell = analysis.cells[cell_id]
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
372
|
+
# A blocked cell is keyed too: a result cached under its key is still its result.
|
|
373
|
+
# One a cycle or a variant collision reaches has no sound upstreams to key it on.
|
|
374
|
+
structural = [
|
|
375
|
+
d
|
|
376
|
+
for blocker in cell.blocked_by
|
|
377
|
+
for d in analysis.cells[blocker].diagnostics
|
|
378
|
+
if d.code in STRUCTURAL_CODES
|
|
379
|
+
]
|
|
380
|
+
if structural:
|
|
381
|
+
raise NotPlannableError(cell_id, structural)
|
|
382
|
+
owns_key = analysis.analyzers[cell.language].owns_key
|
|
353
383
|
fanout = analysis.graph.fanout_cells.get(cell_id)
|
|
354
384
|
if instance is not None:
|
|
355
385
|
if fanout is None:
|
|
@@ -361,7 +391,8 @@ def execution_key(
|
|
|
361
391
|
assembly = _upstream(analysis, cell_id, instance, published)
|
|
362
392
|
inputs: list[tuple[str, str, str]] = []
|
|
363
393
|
resolvers = resolve or {}
|
|
364
|
-
|
|
394
|
+
# A host-keyed cell's inputs are the host's to key.
|
|
395
|
+
for spec in cell.inputs if owns_key else []:
|
|
365
396
|
if spec.kind not in resolvers:
|
|
366
397
|
raise ValueError(f"no resolver for {spec.kind!r} inputs (cell {cell_id})")
|
|
367
398
|
result = resolvers[spec.kind](spec)
|
|
@@ -391,6 +422,8 @@ def execution_key(
|
|
|
391
422
|
"upstream": sorted([address, value] for address, value in assembly.components),
|
|
392
423
|
"inputs": sorted([kind, name, component] for kind, name, component in inputs),
|
|
393
424
|
}
|
|
425
|
+
if not owns_key:
|
|
426
|
+
return HostKeyed(parts=parts)
|
|
394
427
|
if assembly.unresolved:
|
|
395
428
|
return Unavailable(
|
|
396
429
|
reason="unresolved: " + ", ".join(assembly.unresolved),
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import ast
|
|
9
9
|
import builtins
|
|
10
|
+
import functools
|
|
10
11
|
import symtable
|
|
11
12
|
from dataclasses import dataclass, field
|
|
12
13
|
from typing import TYPE_CHECKING
|
|
@@ -641,11 +642,26 @@ def analyze_cell(source: str) -> CellAnalysis:
|
|
|
641
642
|
)
|
|
642
643
|
|
|
643
644
|
|
|
645
|
+
# A notebook is analyzed again on every edit with most cells unchanged, so the
|
|
646
|
+
# per-source work is kept for the sources seen last.
|
|
647
|
+
_MEMO_SIZE = 4096
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
@functools.lru_cache(maxsize=_MEMO_SIZE)
|
|
651
|
+
def _analyzed(source: str) -> CellAnalysis:
|
|
652
|
+
return analyze_cell(source)
|
|
653
|
+
|
|
654
|
+
|
|
644
655
|
def normalize(source: str) -> str:
|
|
645
656
|
"""Canonical text for *source*: its AST round trip, so comments and layout drop out.
|
|
646
657
|
|
|
647
658
|
Source that does not parse falls back to the line strip, which keeps comments.
|
|
648
659
|
"""
|
|
660
|
+
return _normalized(source)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@functools.lru_cache(maxsize=_MEMO_SIZE)
|
|
664
|
+
def _normalized(source: str) -> str:
|
|
649
665
|
try:
|
|
650
666
|
return ast.unparse(ast.parse(source))
|
|
651
667
|
except SyntaxError:
|
|
@@ -659,7 +675,8 @@ class PythonAnalyzer:
|
|
|
659
675
|
owns_key = True
|
|
660
676
|
|
|
661
677
|
def analyze(self, cell: Cell) -> Analyzed:
|
|
662
|
-
|
|
678
|
+
# Memoized: the lists are copied, so the caller may change its own.
|
|
679
|
+
result = _analyzed(cell.source)
|
|
663
680
|
return Analyzed(
|
|
664
681
|
defines=list(result.defines),
|
|
665
682
|
references=list(result.references),
|
|
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
|