dtree-encoding 1.7.0__tar.gz → 1.8.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dtree-encoding
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Summary: DTree: incremental encoding of the topology of directed trees by two projections (Q1, Q2): O(1) reachability, and/or queries on the topology of the tree, dynamic (incremental) updates
5
5
  Author-email: Enis Olgac <enis@digraphs.info>
6
6
  License: MIT
@@ -59,7 +59,9 @@ t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
59
59
  | C# (docs) | Python |
60
60
  |---|---|
61
61
  | `DTree(id)` | `DTree(tree_id="P1")` |
62
- | `Insert(parent, child)` | `insert(parent, child)` |
62
+ | `Insert(parent, child)` | `insert(parent, child)` (last child) |
63
+ | `InsertAfter`, `InsertBefore` | `insert_after(sibling, child)`, `insert_before(sibling, child)` |
64
+ | `AddLine` | `add_line(line)` (`p: c`, `s > c`, `s < c`; also in `from_file`) |
63
65
  | `Contains(node)` | `contains(node)` / `node in t` |
64
66
  | `GetParent(node)` | `get_parent(node)` (`None` for the root) |
65
67
  | `CutNPaste(u, p)` | `cut_n_paste(subtree_root, new_parent)` |
@@ -36,7 +36,9 @@ t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
36
36
  | C# (docs) | Python |
37
37
  |---|---|
38
38
  | `DTree(id)` | `DTree(tree_id="P1")` |
39
- | `Insert(parent, child)` | `insert(parent, child)` |
39
+ | `Insert(parent, child)` | `insert(parent, child)` (last child) |
40
+ | `InsertAfter`, `InsertBefore` | `insert_after(sibling, child)`, `insert_before(sibling, child)` |
41
+ | `AddLine` | `add_line(line)` (`p: c`, `s > c`, `s < c`; also in `from_file`) |
40
42
  | `Contains(node)` | `contains(node)` / `node in t` |
41
43
  | `GetParent(node)` | `get_parent(node)` (`None` for the root) |
42
44
  | `CutNPaste(u, p)` | `cut_n_paste(subtree_root, new_parent)` |
@@ -3,4 +3,4 @@
3
3
  from .dtree import SINK, SOURCE, DList, DNode, DTree, UMax, UMin, VMax, VMin
4
4
 
5
5
  __all__ = ["DTree", "DNode", "DList", "VMin", "VMax", "UMin", "UMax", "SOURCE", "SINK"]
6
- __version__ = "1.7.0"
6
+ __version__ = "1.8.0"
@@ -351,7 +351,12 @@ class DTree:
351
351
  # -- construction ------------------------------------------------------
352
352
  @classmethod
353
353
  def from_file(cls, path: str, tree_id: Optional[str] = None) -> "DTree":
354
- """Read the ``parent: child1; child2`` format used by the .csx examples."""
354
+ """Read the format used by the .csx examples.
355
+
356
+ ``parent: child1; child2`` adds children as last children of ``parent``;
357
+ ``sibling > child1; child2`` inserts them as siblings directly after ``sibling``
358
+ (in that order), ``sibling < child1; child2`` directly before it.
359
+ """
355
360
  import os
356
361
 
357
362
  tree = cls(tree_id or os.path.splitext(os.path.basename(path))[0])
@@ -362,12 +367,27 @@ class DTree:
362
367
  continue
363
368
  if line.startswith("@@"):
364
369
  break
365
- parent, _, rest = line.partition(":")
366
- for child in (c.strip() for c in rest.split(";")):
367
- if child:
368
- tree.insert(parent.strip(), child)
370
+ tree.add_line(line)
369
371
  return tree
370
372
 
373
+ def add_line(self, line: str) -> List[DNode]:
374
+ """Apply one line of the description format (see from_file); returns the new handles."""
375
+ at = min((i for i in (line.find(":"), line.find(">"), line.find("<")) if i >= 0), default=-1)
376
+ if at < 0:
377
+ raise ValueError(f"no ':', '>' or '<' in {line!r}")
378
+ op, anchor = line[at], line[:at].strip()
379
+ children = [c.strip() for c in line[at + 1:].split(";") if c.strip()]
380
+ added = []
381
+ for child in children:
382
+ if op == ":":
383
+ added.append(self.insert(anchor, child))
384
+ elif op == ">":
385
+ added.append(self.insert_after(anchor, child))
386
+ anchor = child # the next one goes after this one
387
+ else:
388
+ added.append(self.insert_before(anchor, child))
389
+ return added
390
+
371
391
  def insert(self, parent, child: str) -> DNode:
372
392
  """Add the edge parent -> child and return the handle of ``child``.
373
393
 
@@ -387,6 +407,56 @@ class DTree:
387
407
  self.edge_log.append(f"({p.name}, {child})")
388
408
  return n
389
409
 
410
+ def insert_after(self, sibling, child: str) -> DNode:
411
+ """Insert ``child`` as a sibling directly after ``sibling`` (same parent) and return its
412
+ handle. In Q1 it goes after the end of the sibling's subtree, in Q2 directly before the
413
+ sibling; no other node moves. The root has no siblings."""
414
+ s = self._sibling(sibling, child)
415
+ p = s.up
416
+ i = p.kids.index(s)
417
+ w = self._subtree_last_node(s)
418
+ n = self._link_node(child, p, i + 1, w.c1, s.c2.prev)
419
+ if i + 1 == len(p.kids) - 1:
420
+ p.last = n # n is now the last node of p's subtree
421
+ self.edge_log.append(f"({s.name} > {child})")
422
+ return n
423
+
424
+ def insert_before(self, sibling, child: str) -> DNode:
425
+ """Insert ``child`` as a sibling directly before ``sibling`` (same parent) and return its
426
+ handle. In Q1 it goes directly before the sibling, in Q2 after the end of the sibling's
427
+ subtree there (directly before the previous sibling; for a first child, after the end
428
+ of the parent's subtree in Q2, found by following first children); no other node moves."""
429
+ s = self._sibling(sibling, child)
430
+ p = s.up
431
+ i = p.kids.index(s)
432
+ q2_after = p.kids[i - 1].c2.prev if i > 0 else self._subtree_first_last_q2(s).c2
433
+ n = self._link_node(child, p, i, s.c1.prev, q2_after)
434
+ self.edge_log.append(f"({s.name} < {child})")
435
+ return n
436
+
437
+ def _sibling(self, sibling, child: str) -> DNode:
438
+ if child in (SOURCE, SINK):
439
+ raise ValueError("SOURCE/SINK markers cannot be tree nodes")
440
+ s = self._node(sibling)
441
+ if s.up is None:
442
+ raise ValueError("the root has no siblings")
443
+ if child in self._nodes:
444
+ raise ValueError(f"child {child!r} is already in the tree")
445
+ return s
446
+
447
+ def _link_node(self, name: str, p: DNode, index: int, q1_after, q2_after) -> DNode:
448
+ """Create a child of p at position ``index`` among p's children, linked after the given cells."""
449
+ n = DNode(name, self)
450
+ c1, c2 = self._q1._new(name), self._q2._new(name)
451
+ self._q2._link_after(c2, q2_after)
452
+ self._q1._link_after(c1, q1_after)
453
+ n.up = p
454
+ p.kids.insert(index, n)
455
+ c1.node = c2.node = n
456
+ n.c1, n.c2 = c1, c2
457
+ self._nodes[name] = n
458
+ return n
459
+
390
460
  def _new_node(self, name: str, p: Optional[DNode]) -> DNode:
391
461
  n = DNode(name, self)
392
462
  q1, q2 = self._q1, self._q2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dtree-encoding
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Summary: DTree: incremental encoding of the topology of directed trees by two projections (Q1, Q2): O(1) reachability, and/or queries on the topology of the tree, dynamic (incremental) updates
5
5
  Author-email: Enis Olgac <enis@digraphs.info>
6
6
  License: MIT
@@ -59,7 +59,9 @@ t = DTree.from_file("Test.csx") # e.g. examples/Test.csx from the repository
59
59
  | C# (docs) | Python |
60
60
  |---|---|
61
61
  | `DTree(id)` | `DTree(tree_id="P1")` |
62
- | `Insert(parent, child)` | `insert(parent, child)` |
62
+ | `Insert(parent, child)` | `insert(parent, child)` (last child) |
63
+ | `InsertAfter`, `InsertBefore` | `insert_after(sibling, child)`, `insert_before(sibling, child)` |
64
+ | `AddLine` | `add_line(line)` (`p: c`, `s > c`, `s < c`; also in `from_file`) |
63
65
  | `Contains(node)` | `contains(node)` / `node in t` |
64
66
  | `GetParent(node)` | `get_parent(node)` (`None` for the root) |
65
67
  | `CutNPaste(u, p)` | `cut_n_paste(subtree_root, new_parent)` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dtree-encoding"
7
- version = "1.7.0"
7
+ version = "1.8.0"
8
8
  description = "DTree: incremental encoding of the topology of directed trees by two projections (Q1, Q2): O(1) reachability, and/or queries on the topology of the tree, dynamic (incremental) updates"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -68,6 +68,18 @@ class Reference:
68
68
  for n in gone:
69
69
  del self.parent[n]
70
70
 
71
+ def insert_after(self, s, v):
72
+ p = self.parent[s]
73
+ sib = self.children[p]
74
+ sib.insert(sib.index(s) + 1, v)
75
+ self.parent[v] = p
76
+
77
+ def insert_before(self, s, v):
78
+ p = self.parent[s]
79
+ sib = self.children[p]
80
+ sib.insert(sib.index(s), v)
81
+ self.parent[v] = p
82
+
71
83
  def move(self, u, p):
72
84
  old = self.parent[u]
73
85
  self.children[old].remove(u)
@@ -559,6 +571,136 @@ class TestAgainstReference(unittest.TestCase):
559
571
  check(self, t, ref)
560
572
 
561
573
 
574
+ class TestSiblingInsert(unittest.TestCase):
575
+ def test_example(self):
576
+ t = DTree()
577
+ t.insert("A", "B")
578
+ t.insert("A", "D")
579
+ t.insert("B", "C")
580
+ t.insert_after("B", "X") # A: B, X, D
581
+ t.insert_before("B", "W") # A: W, B, X, D
582
+ t.insert_before("C", "C0") # B: C0, C
583
+ self.assertEqual(t.children("A"), ["W", "B", "X", "D"])
584
+ self.assertEqual(t.children("B"), ["C0", "C"])
585
+ self.assertEqual(t.q1, ["A", "W", "B", "C0", "C", "X", "D"])
586
+ self.assertEqual(t.q2, ["A", "D", "X", "B", "C", "C0", "W"])
587
+ self.assertTrue(t.can_reach("A", "X") and not t.can_reach("B", "X"))
588
+ self.assertEqual(t.subtree_last("A"), "D")
589
+
590
+ def test_rules(self):
591
+ t = DTree()
592
+ t.insert("A", "B")
593
+ with self.assertRaises(ValueError):
594
+ t.insert_after("A", "Z") # the root has no siblings
595
+ with self.assertRaises(ValueError):
596
+ t.insert_before("A", "Z")
597
+ with self.assertRaises(ValueError):
598
+ t.insert_after("B", "A") # already in the tree
599
+ with self.assertRaises(KeyError):
600
+ t.insert_after("nobody", "Z")
601
+ n = t.insert_after(t.get_node("B"), "C") # handles work too
602
+ self.assertIsInstance(n, DNode)
603
+ self.assertEqual(t.children("A"), ["B", "C"])
604
+
605
+ def test_example_file(self):
606
+ t = DTree.from_file(os.path.join(EXAMPLES, "siblings.csx"))
607
+ self.assertEqual(t.children("01"), ["02", "04", "06", "05"])
608
+ self.assertEqual(t.q1, ["01", "02", "03a", "03", "04", "06", "05"])
609
+ self.assertEqual(t.q2, ["01", "05", "06", "04", "02", "03", "03a"])
610
+
611
+ def test_description_lines(self):
612
+ t = DTree()
613
+ t.add_line("A: B; D")
614
+ t.add_line("B > X; Y") # X after B, Y after X
615
+ t.add_line("D < W1; W2") # W1 before D, W2 before D
616
+ t.add_line("B: C")
617
+ self.assertEqual(t.children("A"), ["B", "X", "Y", "W1", "W2", "D"])
618
+ self.assertEqual(t.children("B"), ["C"])
619
+
620
+ def test_random_mix(self):
621
+ # all three kinds of insertion, mixed with deletes and moves, against the reference
622
+ import dtree.dtree as impl
623
+ for budget in (impl._BUDGET, 0):
624
+ saved, impl._BUDGET = impl._BUDGET, budget
625
+ try:
626
+ for seed in range(12):
627
+ t, ref, rnd = random_tree(20, seed)
628
+ k = 0
629
+ for _ in range(100):
630
+ nodes = ref.nodes()
631
+ inner = [n for n in nodes if n != ref.root]
632
+ r = rnd.random()
633
+ name = f"x{k}"
634
+ if r < 0.25:
635
+ p = rnd.choice(nodes)
636
+ t.insert(p, name)
637
+ ref.insert(p, name)
638
+ k += 1
639
+ elif r < 0.5 and inner:
640
+ s = rnd.choice(inner)
641
+ t.insert_after(s, name)
642
+ ref.insert_after(s, name)
643
+ k += 1
644
+ elif r < 0.75 and inner:
645
+ s = rnd.choice(inner)
646
+ t.insert_before(s, name)
647
+ ref.insert_before(s, name)
648
+ k += 1
649
+ elif r < 0.85 and len(inner) > 10:
650
+ v = rnd.choice(inner)
651
+ t.delete(v)
652
+ ref.delete(v)
653
+ elif len(inner) > 10:
654
+ u = rnd.choice(inner)
655
+ banned = set(t.subtree(u))
656
+ p = rnd.choice([n for n in nodes if n not in banned])
657
+ t.cut_n_paste(u, p)
658
+ ref.move(u, p)
659
+ check(self, t, ref)
660
+ for v in rnd.sample(ref.nodes(), 3):
661
+ sub_v = [w for w in ref.nodes() if w == v or v == ref.root or v in ref_ancestors(ref.parent, w)]
662
+ self.assertEqual(t.subtree_last(v), [w for w in t.q1 if w in sub_v][-1])
663
+ finally:
664
+ impl._BUDGET = saved
665
+
666
+ def test_same_gap_many_times(self):
667
+ # repeated insertion into the same gap from both sides: positions stay strictly increasing
668
+ t, ref = DTree(), Reference()
669
+ t.insert("r", "a")
670
+ ref.insert("r", "a")
671
+ t.insert("r", "b")
672
+ ref.insert("r", "b")
673
+ left, right = "a", "b"
674
+ for i in range(400):
675
+ if i % 2:
676
+ t.insert_after(left, f"l{i}")
677
+ ref.insert_after(left, f"l{i}")
678
+ left = f"l{i}"
679
+ else:
680
+ t.insert_before(right, f"r{i}")
681
+ ref.insert_before(right, f"r{i}")
682
+ right = f"r{i}"
683
+ check(self, t, ref)
684
+
685
+ def test_constructed_nodes_stay_valid(self):
686
+ t, ref, rnd = random_tree(60, 3)
687
+ S = rnd.sample(ref.nodes(), 6)
688
+ um, vm = t.umin(S), t.vmax(S)
689
+ for i in range(60):
690
+ s = rnd.choice([n for n in ref.nodes() if n != ref.root])
691
+ if i % 2:
692
+ t.insert_after(s, f"y{i}")
693
+ ref.insert_after(s, f"y{i}")
694
+ else:
695
+ t.insert_before(s, f"y{i}")
696
+ ref.insert_before(s, f"y{i}")
697
+ for v in ref.nodes():
698
+ anc = set(ref_ancestors(ref.parent, v)[1:]) | ({ref.root} if v != ref.root else set())
699
+ self.assertEqual(um.reached_by_some(v), any(m in anc for m in S), v)
700
+ below = {w for w in ref.nodes() if w != v and (v == ref.root or v in ref_ancestors(ref.parent, w))}
701
+ self.assertEqual(vm.reaches_some(v), any(m in below for m in S), v)
702
+
703
+
562
704
  class TestNoSizeLimit(unittest.TestCase):
563
705
  def test_deep_chain(self):
564
706
  t = DTree()
File without changes