iker-python-common 1.0.63__tar.gz → 1.0.64__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.
Files changed (68) hide show
  1. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/PKG-INFO +1 -1
  2. iker_python_common-1.0.63/src/iker/common/utils/sequtils.py → iker_python_common-1.0.64/src/iker/common/utils/iterutils.py +168 -0
  3. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/randutils.py +1 -1
  4. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/shutils.py +1 -1
  5. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker_python_common.egg-info/PKG-INFO +1 -1
  6. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker_python_common.egg-info/SOURCES.txt +2 -2
  7. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/argutils_test.py +1 -1
  8. iker_python_common-1.0.63/test/iker_tests/common/utils/sequtils_test.py → iker_python_common-1.0.64/test/iker_tests/common/utils/iterutils_test.py +696 -6
  9. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/span_test.py +1 -1
  10. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/.editorconfig +0 -0
  11. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/.github/workflows/pr.yml +0 -0
  12. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/.github/workflows/push.yml +0 -0
  13. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/.gitignore +0 -0
  14. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/MANIFEST.in +0 -0
  15. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/README.md +0 -0
  16. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/VERSION +0 -0
  17. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/pyproject.toml +0 -0
  18. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/config/config.cfg +0 -0
  19. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/csv/data.csv +0 -0
  20. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/csv/data.tsv +0 -0
  21. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
  22. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
  23. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
  24. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  25. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  26. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  27. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  28. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.foo/file.bar +0 -0
  29. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.foo/file.baz +0 -0
  30. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/resources/unittest/shutils/dir.foo/file.foo +0 -0
  31. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/setup.cfg +0 -0
  32. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/setup.py +0 -0
  33. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/__init__.py +0 -0
  34. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/__init__.py +0 -0
  35. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/argutils.py +0 -0
  36. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/config.py +0 -0
  37. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/csv.py +0 -0
  38. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/dbutils.py +0 -0
  39. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/dtutils.py +0 -0
  40. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/funcutils.py +0 -0
  41. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/jsonutils.py +0 -0
  42. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/logger.py +0 -0
  43. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/numutils.py +0 -0
  44. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/retry.py +0 -0
  45. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/span.py +0 -0
  46. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/strutils.py +0 -0
  47. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/testutils.py +0 -0
  48. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker/common/utils/typeutils.py +0 -0
  49. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
  50. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker_python_common.egg-info/not-zip-safe +0 -0
  51. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker_python_common.egg-info/requires.txt +0 -0
  52. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/src/iker_python_common.egg-info/top_level.txt +0 -0
  53. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_test.py +0 -0
  54. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/__init__.py +0 -0
  55. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/config_test.py +0 -0
  56. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/csv_test.py +0 -0
  57. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/dbutils_test.py +0 -0
  58. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/dtutils_test.py +0 -0
  59. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/funcutils_test.py +0 -0
  60. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/jsonutils_test.py +0 -0
  61. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/logger_test.py +0 -0
  62. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/numutils_test.py +0 -0
  63. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/randutils_test.py +0 -0
  64. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/retry_test.py +0 -0
  65. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/shutils_test.py +0 -0
  66. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/strutils_test.py +0 -0
  67. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/testutils_test.py +0 -0
  68. {iker_python_common-1.0.63 → iker_python_common-1.0.64}/test/iker_tests/common/utils/typeutils_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.63
3
+ Version: 1.0.64
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -27,6 +27,15 @@ __all__ = [
27
27
  "chunk_between",
28
28
  "chunk_with_key",
29
29
  "merge_chunks",
30
+ "dicttree",
31
+ "dicttree_value",
32
+ "dicttree_subtree",
33
+ "dicttree_children",
34
+ "dicttree_lineage",
35
+ "dicttree_make",
36
+ "dicttree_add",
37
+ "dicttree_remove",
38
+ "dicttree_purge",
30
39
  "Seq",
31
40
  "seq",
32
41
  ]
@@ -539,6 +548,165 @@ def merge_chunks[T](
539
548
  chunk_between(chunks, lambda a, b: not merge_func(a, b)))
540
549
 
541
550
 
551
+ # Dicttree, a tree structure implemented using nested dictionaries,
552
+ # where each node can have an optional value and child nodes.
553
+ type dicttree[K, V] = dict[K, tuple[V | None, dicttree[K, V]]]
554
+
555
+
556
+ def dicttree_value[K, V](tree: dicttree[K, V], path: list[K]) -> V | None:
557
+ """
558
+ Gets the value stored at the specified path in the dicttree.
559
+
560
+ :param tree: the dicttree to retrieve the value from.
561
+ :param path: the list of keys representing the path to the desired value.
562
+ :return: the value at the specified path, or ``None`` if the path does not exist.
563
+ """
564
+ value = None
565
+ for key in path:
566
+ if key not in tree:
567
+ return None
568
+ value, tree = tree[key]
569
+ return value
570
+
571
+
572
+ def dicttree_subtree[K, V](tree: dicttree[K, V], path: list[K]) -> dicttree[K, V] | None:
573
+ """
574
+ Gets the subtree located at the specified path in the dicttree.
575
+
576
+ :param tree: the dicttree to retrieve the subtree from.
577
+ :param path: the list of keys representing the path to the desired subtree.
578
+ :return: the subtree at the specified path, or ``None`` if the path does not exist.
579
+ """
580
+ for key in path:
581
+ if key not in tree:
582
+ return None
583
+ _, tree = tree[key]
584
+ return tree
585
+
586
+
587
+ def dicttree_children[K, V](tree: dicttree[K, V], *, leaves_only: bool = False) -> Generator[V, None, None]:
588
+ """
589
+ Yields all values in the dicttree. If ``leaves_only`` is ``True``, only yields values at leaf nodes.
590
+
591
+ :param tree: the dicttree to traverse.
592
+ :param leaves_only: whether to yield only values at leaf nodes.
593
+ :return: a generator yielding all values in the dicttree.
594
+ """
595
+ for value, subtree in tree.values():
596
+ if value is not None and (not leaves_only or not subtree):
597
+ yield value
598
+ yield from dicttree_children(subtree, leaves_only=leaves_only)
599
+
600
+
601
+ def dicttree_lineage[K, V](tree: dicttree[K, V], path: list[K]) -> Generator[V, None, None]:
602
+ """
603
+ Yields all values along the specified path in the dicttree.
604
+
605
+ :param tree: the dicttree to traverse.
606
+ :param path: the list of keys representing the path to follow.
607
+ :return: a generator yielding all values along the specified path.
608
+ """
609
+ for key in path:
610
+ if key not in tree:
611
+ return
612
+ value, tree = tree[key]
613
+ if value is not None:
614
+ yield value
615
+
616
+
617
+ def dicttree_make[K, V](tree: dicttree[K, V], path: list[K]) -> dicttree[K, V]:
618
+ """
619
+ Ensures that the specified path exists in the dicttree, creating any missing nodes along the way.
620
+
621
+ :param tree: the dicttree to modify.
622
+ :param path: the list of keys representing the path to create.
623
+ :return: the subtree at the end of the created path.
624
+ """
625
+ for key in path:
626
+ tree.setdefault(key, (None, {}))
627
+ _, tree = tree[key]
628
+ return tree
629
+
630
+
631
+ def dicttree_add[K, V](
632
+ tree: dicttree[K, V],
633
+ path: list[K],
634
+ value: V,
635
+ *,
636
+ create_prefix: bool = True,
637
+ overwrite: bool = False,
638
+ ) -> dicttree[K, V]:
639
+ """
640
+ Adds a value at the specified path in the dicttree. If ``create_prefix`` is ``True``, any missing nodes along the
641
+ path are created. If ``overwrite`` is ``False``, raises an error if the path already exists.
642
+
643
+ :param tree: the dicttree to modify.
644
+ :param path: the list of keys representing the path to add the value to.
645
+ :param value: the value to add at the specified path.
646
+ :param create_prefix: whether to create missing nodes along the path.
647
+ :param overwrite: whether to overwrite an existing value at the path.
648
+ :return: the modified dicttree.
649
+ """
650
+ if len(path) == 0:
651
+ raise ValueError("path cannot be empty")
652
+ *prefix, key = path
653
+ if create_prefix:
654
+ subtree = dicttree_make(tree, prefix)
655
+ else:
656
+ subtree = dicttree_subtree(tree, prefix)
657
+ if subtree is None:
658
+ raise ValueError("prefix path does not exist in dicttree")
659
+ if not overwrite and dicttree_value(subtree, [key]) is not None:
660
+ raise ValueError("path already exists in dicttree")
661
+ else:
662
+ subtree[key] = (value, dicttree_subtree(subtree, [key]) or {})
663
+ return tree
664
+
665
+
666
+ def dicttree_remove[K, V](
667
+ tree: dicttree[K, V],
668
+ path: list[K],
669
+ *,
670
+ recursive: bool = False,
671
+ ) -> dicttree[K, V]:
672
+ """
673
+ Removes the value at the specified path in the dicttree. If ``recursive`` is ``True``, removes the entire subtree
674
+ at that path; otherwise, only removes the value, leaving any child nodes intact.
675
+
676
+ :param tree: the dicttree to modify.
677
+ :param path: the list of keys representing the path to remove the value from.
678
+ :param recursive: whether to remove the entire subtree at the path.
679
+ :return: the modified dicttree.
680
+ """
681
+ if len(path) == 0:
682
+ raise ValueError("path cannot be empty")
683
+ *prefix, key = path
684
+ subtree = dicttree_subtree(tree, prefix)
685
+ if subtree is None:
686
+ raise ValueError("prefix path does not exist in dicttree")
687
+ if dicttree_value(subtree, [key]) is None:
688
+ raise ValueError("path does not exist in dicttree")
689
+ else:
690
+ subtree[key] = (None, {} if recursive else dicttree_subtree(subtree, [key]) or {})
691
+ return tree
692
+
693
+
694
+ def dicttree_purge[K, V](tree: dicttree[K, V]) -> dicttree[K, V]:
695
+ """
696
+ Recursively removes all nodes in the dicttree that have no value and no children.
697
+
698
+ :param tree: the dicttree to purge.
699
+ :return: the purged dicttree.
700
+ """
701
+ for key in list(tree.keys()):
702
+ value, subtree = tree[key]
703
+ dicttree_purge(subtree)
704
+ if subtree or value is not None:
705
+ continue
706
+ del tree[key]
707
+ return tree
708
+
709
+
542
710
  class Seq[T](Sequence[T], Sized):
543
711
  def __init__(self, data: Iterable[T]):
544
712
  if isinstance(data, Seq):
@@ -8,8 +8,8 @@ from typing import overload
8
8
 
9
9
  from iker.common.utils.dtutils import dt_utc_max, dt_utc_min
10
10
  from iker.common.utils.funcutils import memorized, singleton
11
+ from iker.common.utils.iterutils import head_or_none
11
12
  from iker.common.utils.jsonutils import JsonType
12
- from iker.common.utils.sequtils import head_or_none
13
13
 
14
14
  __all__ = [
15
15
  "max_int",
@@ -4,7 +4,7 @@ import shutil
4
4
  from typing import Protocol
5
5
 
6
6
  from iker.common.utils import logger
7
- from iker.common.utils.sequtils import last, last_or_none, tail_iter
7
+ from iker.common.utils.iterutils import last, last_or_none, tail_iter
8
8
  from iker.common.utils.strutils import is_empty
9
9
 
10
10
  __all__ = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.63
3
+ Version: 1.0.64
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -28,12 +28,12 @@ src/iker/common/utils/csv.py
28
28
  src/iker/common/utils/dbutils.py
29
29
  src/iker/common/utils/dtutils.py
30
30
  src/iker/common/utils/funcutils.py
31
+ src/iker/common/utils/iterutils.py
31
32
  src/iker/common/utils/jsonutils.py
32
33
  src/iker/common/utils/logger.py
33
34
  src/iker/common/utils/numutils.py
34
35
  src/iker/common/utils/randutils.py
35
36
  src/iker/common/utils/retry.py
36
- src/iker/common/utils/sequtils.py
37
37
  src/iker/common/utils/shutils.py
38
38
  src/iker/common/utils/span.py
39
39
  src/iker/common/utils/strutils.py
@@ -53,12 +53,12 @@ test/iker_tests/common/utils/csv_test.py
53
53
  test/iker_tests/common/utils/dbutils_test.py
54
54
  test/iker_tests/common/utils/dtutils_test.py
55
55
  test/iker_tests/common/utils/funcutils_test.py
56
+ test/iker_tests/common/utils/iterutils_test.py
56
57
  test/iker_tests/common/utils/jsonutils_test.py
57
58
  test/iker_tests/common/utils/logger_test.py
58
59
  test/iker_tests/common/utils/numutils_test.py
59
60
  test/iker_tests/common/utils/randutils_test.py
60
61
  test/iker_tests/common/utils/retry_test.py
61
- test/iker_tests/common/utils/sequtils_test.py
62
62
  test/iker_tests/common/utils/shutils_test.py
63
63
  test/iker_tests/common/utils/span_test.py
64
64
  test/iker_tests/common/utils/strutils_test.py
@@ -6,7 +6,7 @@ import ddt
6
6
 
7
7
  from iker.common.utils.argutils import ParserTree
8
8
  from iker.common.utils.argutils import argparse_spec, make_argparse
9
- from iker.common.utils.sequtils import seq
9
+ from iker.common.utils.iterutils import seq
10
10
 
11
11
 
12
12
  def dummy_parser_tree():
@@ -5,12 +5,16 @@ from typing import Self
5
5
 
6
6
  import ddt
7
7
 
8
- from iker.common.utils.sequtils import Seq
9
- from iker.common.utils.sequtils import batched, deduped, flatten, grouped
10
- from iker.common.utils.sequtils import chunk, chunk_between, chunk_with_key, merge_chunks
11
- from iker.common.utils.sequtils import head, last
12
- from iker.common.utils.sequtils import init, init_iter, tail, tail_iter
13
- from iker.common.utils.sequtils import seq
8
+ from iker.common.utils.iterutils import Seq
9
+ from iker.common.utils.iterutils import batched, deduped, flatten, grouped
10
+ from iker.common.utils.iterutils import chunk, chunk_between, chunk_with_key, merge_chunks
11
+ from iker.common.utils.iterutils import dicttree
12
+ from iker.common.utils.iterutils import dicttree_add, dicttree_purge, dicttree_remove
13
+ from iker.common.utils.iterutils import dicttree_children, dicttree_lineage
14
+ from iker.common.utils.iterutils import dicttree_subtree, dicttree_value
15
+ from iker.common.utils.iterutils import head, last
16
+ from iker.common.utils.iterutils import init, init_iter, tail, tail_iter
17
+ from iker.common.utils.iterutils import seq
14
18
 
15
19
 
16
20
  @ddt.ddt
@@ -937,6 +941,692 @@ class SeqUtilsTest(unittest.TestCase):
937
941
  self.assertEqual(expect, list(merge_chunks(data, merge_func=merge_func, drop_exclusive_end=True)))
938
942
 
939
943
 
944
+ @ddt.ddt
945
+ class DicttreeTest(unittest.TestCase):
946
+ data_dicttree_value = [
947
+ ({}, [[]], [None]),
948
+ (
949
+ {
950
+ "a": (1, {}),
951
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
952
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
953
+ },
954
+ [
955
+ ["a"],
956
+ ["b"],
957
+ ["b", "c"],
958
+ ["b", "d"],
959
+ ["b", "d", "e"],
960
+ ["b", "d", "f"],
961
+ ["g"],
962
+ ["g", "h"],
963
+ ["g", "i"],
964
+ ],
965
+ [1, 2, 3, 4, 5, 6, 7, 8, 9],
966
+ ),
967
+ (
968
+ {
969
+ "a": (1, {}),
970
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
971
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
972
+ },
973
+ [
974
+ ["b", "c", "d"],
975
+ ["b", "c", "d", "e"],
976
+ ["b", "c", "d", "e", "f"],
977
+ ["c"],
978
+ ["d"],
979
+ ["e"],
980
+ ["f"],
981
+ ["g", "h", "i"],
982
+ ["h"],
983
+ ["i"],
984
+ ],
985
+ [None, None, None, None, None, None, None, None, None, None],
986
+ ),
987
+ (
988
+ {
989
+ "a": (1, {}),
990
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
991
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
992
+ },
993
+ [
994
+ ["a"],
995
+ ["b"],
996
+ ["b", "c"],
997
+ ["b", "d"],
998
+ ["b", "d", "e"],
999
+ ["b", "d", "f"],
1000
+ ["g"],
1001
+ ["g", "h"],
1002
+ ["g", "i"],
1003
+ ],
1004
+ [1, 2, None, 4, None, 6, None, 8, None],
1005
+ ),
1006
+ (
1007
+ {
1008
+ "a": (1, {}),
1009
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1010
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1011
+ },
1012
+ [
1013
+ ["b", "c", "d"],
1014
+ ["b", "c", "d", "e"],
1015
+ ["b", "c", "d", "e", "f"],
1016
+ ["c"],
1017
+ ["d"],
1018
+ ["e"],
1019
+ ["f"],
1020
+ ["g", "h", "i"],
1021
+ ["h"],
1022
+ ["i"],
1023
+ ],
1024
+ [None, None, None, None, None, None, None, None, None, None],
1025
+ ),
1026
+ ]
1027
+
1028
+ @ddt.idata(data_dicttree_value)
1029
+ @ddt.unpack
1030
+ def test_dicttree_value(self, tree: dicttree[str, int], paths, expects):
1031
+ for expect, path in zip(expects, paths):
1032
+ self.assertEqual(expect, dicttree_value(tree, path))
1033
+
1034
+ data_dicttree_subtree = [
1035
+ ({}, [[]], [{}]),
1036
+ (
1037
+ {
1038
+ "a": (1, {}),
1039
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1040
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1041
+ },
1042
+ [
1043
+ [],
1044
+ ["a"],
1045
+ ["b"],
1046
+ ["b", "c"],
1047
+ ["b", "d"],
1048
+ ["b", "d", "e"],
1049
+ ["b", "d", "f"],
1050
+ ["g"],
1051
+ ["g", "h"],
1052
+ ["g", "i"],
1053
+ ],
1054
+ [
1055
+ {
1056
+ "a": (1, {}),
1057
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1058
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1059
+ },
1060
+ {},
1061
+ {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})},
1062
+ {},
1063
+ {"e": (5, {}), "f": (6, {})},
1064
+ {},
1065
+ {},
1066
+ {"h": (8, {}), "i": (9, {})},
1067
+ {},
1068
+ {},
1069
+ ],
1070
+ ),
1071
+ (
1072
+ {
1073
+ "a": (1, {}),
1074
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1075
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1076
+ },
1077
+ [
1078
+ ["b", "c", "d"],
1079
+ ["b", "c", "d", "e"],
1080
+ ["b", "c", "d", "e", "f"],
1081
+ ["c"],
1082
+ ["d"],
1083
+ ["e"],
1084
+ ["f"],
1085
+ ["g", "h", "i"],
1086
+ ["h"],
1087
+ ["i"],
1088
+ ],
1089
+ [None, None, None, None, None, None, None, None, None, None],
1090
+ ),
1091
+ (
1092
+ {
1093
+ "a": (1, {}),
1094
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1095
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1096
+ },
1097
+ [
1098
+ [],
1099
+ ["a"],
1100
+ ["b"],
1101
+ ["b", "c"],
1102
+ ["b", "d"],
1103
+ ["b", "d", "e"],
1104
+ ["b", "d", "f"],
1105
+ ["g"],
1106
+ ["g", "h"],
1107
+ ["g", "i"],
1108
+ ],
1109
+ [
1110
+ {
1111
+ "a": (1, {}),
1112
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1113
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1114
+ },
1115
+ {},
1116
+ {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})},
1117
+ {},
1118
+ {"e": (None, {}), "f": (6, {})},
1119
+ {},
1120
+ {},
1121
+ {"h": (8, {}), "i": (None, {})},
1122
+ {},
1123
+ {},
1124
+ ],
1125
+ ),
1126
+ (
1127
+ {
1128
+ "a": (1, {}),
1129
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1130
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1131
+ },
1132
+ [
1133
+ ["b", "c", "d"],
1134
+ ["b", "c", "d", "e"],
1135
+ ["b", "c", "d", "e", "f"],
1136
+ ["c"],
1137
+ ["d"],
1138
+ ["e"],
1139
+ ["f"],
1140
+ ["g", "h", "i"],
1141
+ ["h"],
1142
+ ["i"],
1143
+ ],
1144
+ [None, None, None, None, None, None, None, None, None, None],
1145
+ ),
1146
+ ]
1147
+
1148
+ @ddt.idata(data_dicttree_subtree)
1149
+ @ddt.unpack
1150
+ def test_dicttree_subtree(self, tree: dicttree[str, int], paths, expects):
1151
+ for expect, path in zip(expects, paths):
1152
+ self.assertEqual(expect, dicttree_subtree(tree, path))
1153
+
1154
+ data_dicttree_children = [
1155
+ ({}, [[]], False, [set()]),
1156
+ (
1157
+ {
1158
+ "a": (1, {}),
1159
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1160
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1161
+ },
1162
+ [
1163
+ [],
1164
+ ["a"],
1165
+ ["b"],
1166
+ ["b", "c"],
1167
+ ["b", "d"],
1168
+ ["b", "d", "e"],
1169
+ ["b", "d", "f"],
1170
+ ["g"],
1171
+ ["g", "h"],
1172
+ ["g", "i"],
1173
+ ],
1174
+ False,
1175
+ [{1, 2, 3, 4, 5, 6, 7, 8, 9}, set(), {3, 4, 5, 6}, set(), {5, 6}, set(), set(), {8, 9}, set(), set()],
1176
+ ),
1177
+ (
1178
+ {
1179
+ "a": (1, {}),
1180
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1181
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1182
+ },
1183
+ [
1184
+ ["b", "c", "d"],
1185
+ ["b", "c", "d", "e"],
1186
+ ["b", "c", "d", "e", "f"],
1187
+ ["c"],
1188
+ ["d"],
1189
+ ["e"],
1190
+ ["f"],
1191
+ ["g", "h", "i"],
1192
+ ["h"],
1193
+ ["i"],
1194
+ ],
1195
+ False,
1196
+ [set(), set(), set(), set(), set(), set(), set(), set(), set(), set()],
1197
+ ),
1198
+ (
1199
+ {
1200
+ "a": (1, {}),
1201
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1202
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1203
+ },
1204
+ [
1205
+ [],
1206
+ ["a"],
1207
+ ["b"],
1208
+ ["b", "c"],
1209
+ ["b", "d"],
1210
+ ["b", "d", "e"],
1211
+ ["b", "d", "f"],
1212
+ ["g"],
1213
+ ["g", "h"],
1214
+ ["g", "i"],
1215
+ ],
1216
+ False,
1217
+ [{1, 2, 4, 6, 8}, set(), {4, 6}, set(), {6}, set(), set(), {8}, set(), set()],
1218
+ ),
1219
+ (
1220
+ {
1221
+ "a": (1, {}),
1222
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1223
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1224
+ },
1225
+ [
1226
+ ["b", "c", "d"],
1227
+ ["b", "c", "d", "e"],
1228
+ ["b", "c", "d", "e", "f"],
1229
+ ["c"],
1230
+ ["d"],
1231
+ ["e"],
1232
+ ["f"],
1233
+ ["g", "h", "i"],
1234
+ ["h"],
1235
+ ["i"],
1236
+ ],
1237
+ False,
1238
+ [set(), set(), set(), set(), set(), set(), set(), set(), set(), set()],
1239
+ ),
1240
+ (
1241
+ {
1242
+ "a": (1, {}),
1243
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1244
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1245
+ },
1246
+ [
1247
+ [],
1248
+ ["a"],
1249
+ ["b"],
1250
+ ["b", "c"],
1251
+ ["b", "d"],
1252
+ ["b", "d", "e"],
1253
+ ["b", "d", "f"],
1254
+ ["g"],
1255
+ ["g", "h"],
1256
+ ["g", "i"],
1257
+ ],
1258
+ True,
1259
+ [{1, 3, 5, 6, 8, 9}, set(), {3, 5, 6}, set(), {5, 6}, set(), set(), {8, 9}, set(), set()],
1260
+ ),
1261
+ (
1262
+ {
1263
+ "a": (1, {}),
1264
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1265
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1266
+ },
1267
+ [
1268
+ ["b", "c", "d"],
1269
+ ["b", "c", "d", "e"],
1270
+ ["b", "c", "d", "e", "f"],
1271
+ ["c"],
1272
+ ["d"],
1273
+ ["e"],
1274
+ ["f"],
1275
+ ["g", "h", "i"],
1276
+ ["h"],
1277
+ ["i"],
1278
+ ],
1279
+ True,
1280
+ [set(), set(), set(), set(), set(), set(), set(), set(), set(), set()],
1281
+ ),
1282
+ (
1283
+ {
1284
+ "a": (1, {}),
1285
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1286
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1287
+ },
1288
+ [
1289
+ [],
1290
+ ["a"],
1291
+ ["b"],
1292
+ ["b", "c"],
1293
+ ["b", "d"],
1294
+ ["b", "d", "e"],
1295
+ ["b", "d", "f"],
1296
+ ["g"],
1297
+ ["g", "h"],
1298
+ ["g", "i"],
1299
+ ],
1300
+ True,
1301
+ [{1, 6, 8}, set(), {6}, set(), {6}, set(), set(), {8}, set(), set()],
1302
+ ),
1303
+ (
1304
+ {
1305
+ "a": (1, {}),
1306
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1307
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1308
+ },
1309
+ [
1310
+ ["b", "c", "d"],
1311
+ ["b", "c", "d", "e"],
1312
+ ["b", "c", "d", "e", "f"],
1313
+ ["c"],
1314
+ ["d"],
1315
+ ["e"],
1316
+ ["f"],
1317
+ ["g", "h", "i"],
1318
+ ["h"],
1319
+ ["i"],
1320
+ ],
1321
+ True,
1322
+ [set(), set(), set(), set(), set(), set(), set(), set(), set(), set()],
1323
+ ),
1324
+ ]
1325
+
1326
+ @ddt.idata(data_dicttree_children)
1327
+ @ddt.unpack
1328
+ def test_dicttree_children(self, tree: dicttree[str, int], paths, leaves_only, expects):
1329
+ for expect, path in zip(expects, paths):
1330
+ subtree = dicttree_subtree(tree, path)
1331
+ if subtree is None:
1332
+ self.assertEqual(expect, set())
1333
+ else:
1334
+ self.assertEqual(expect, set(dicttree_children(subtree, leaves_only=leaves_only)))
1335
+
1336
+ data_dicttree_lineage = [
1337
+ ({}, [[]], [[]]),
1338
+ (
1339
+ {
1340
+ "a": (1, {}),
1341
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1342
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1343
+ },
1344
+ [
1345
+ [],
1346
+ ["a"],
1347
+ ["b"],
1348
+ ["b", "c"],
1349
+ ["b", "d"],
1350
+ ["b", "d", "e"],
1351
+ ["b", "d", "f"],
1352
+ ["g"],
1353
+ ["g", "h"],
1354
+ ["g", "i"],
1355
+ ],
1356
+ [[], [1], [2], [2, 3], [2, 4], [2, 4, 5], [2, 4, 6], [7], [7, 8], [7, 9]],
1357
+ ),
1358
+ (
1359
+ {
1360
+ "a": (1, {}),
1361
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1362
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1363
+ },
1364
+ [
1365
+ ["b", "c", "d"],
1366
+ ["b", "c", "d", "e"],
1367
+ ["b", "c", "d", "e", "f"],
1368
+ ["c"],
1369
+ ["d"],
1370
+ ["e"],
1371
+ ["f"],
1372
+ ["g", "h", "i"],
1373
+ ["h"],
1374
+ ["i"],
1375
+ ],
1376
+ [[2, 3], [2, 3], [2, 3], [], [], [], [], [7, 8], [], []],
1377
+ ),
1378
+ (
1379
+ {
1380
+ "a": (1, {}),
1381
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1382
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1383
+ },
1384
+ [
1385
+ [],
1386
+ ["a"],
1387
+ ["b"],
1388
+ ["b", "c"],
1389
+ ["b", "d"],
1390
+ ["b", "d", "e"],
1391
+ ["b", "d", "f"],
1392
+ ["g"],
1393
+ ["g", "h"],
1394
+ ["g", "i"],
1395
+ ],
1396
+ [[], [1], [2], [2], [2, 4], [2, 4], [2, 4, 6], [], [8], []],
1397
+ ),
1398
+ (
1399
+ {
1400
+ "a": (1, {}),
1401
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1402
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1403
+ },
1404
+ [
1405
+ ["b", "c", "d"],
1406
+ ["b", "c", "d", "e"],
1407
+ ["b", "c", "d", "e", "f"],
1408
+ ["c"],
1409
+ ["d"],
1410
+ ["e"],
1411
+ ["f"],
1412
+ ["g", "h", "i"],
1413
+ ["h"],
1414
+ ["i"],
1415
+ ],
1416
+ [[2], [2], [2], [], [], [], [], [8], [], []],
1417
+ ),
1418
+ ]
1419
+
1420
+ @ddt.idata(data_dicttree_lineage)
1421
+ @ddt.unpack
1422
+ def test_dicttree_lineage(self, tree: dicttree[str, int], paths, expects):
1423
+ for expect, path in zip(expects, paths):
1424
+ self.assertEqual(expect, list(dicttree_lineage(tree, path)))
1425
+
1426
+ data_dicttree_purge = [
1427
+ ({}, {}),
1428
+ ({"a": (1, {})}, {"a": (1, {})}),
1429
+ ({"a": (None, {})}, {}),
1430
+ (
1431
+ {
1432
+ "a": (1, {}),
1433
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1434
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1435
+ },
1436
+ {
1437
+ "a": (1, {}),
1438
+ "b": (2, {"c": (3, {}), "d": (4, {"e": (5, {}), "f": (6, {})})}),
1439
+ "g": (7, {"h": (8, {}), "i": (9, {})}),
1440
+ },
1441
+ ),
1442
+ (
1443
+ {
1444
+ "a": (1, {}),
1445
+ "b": (2, {"c": (None, {}), "d": (4, {"e": (None, {}), "f": (6, {})})}),
1446
+ "g": (None, {"h": (8, {}), "i": (None, {})}),
1447
+ },
1448
+ {
1449
+ "a": (1, {}),
1450
+ "b": (2, {"d": (4, {"f": (6, {})})}),
1451
+ "g": (None, {"h": (8, {})}),
1452
+ },
1453
+ ),
1454
+ ]
1455
+
1456
+ @ddt.idata(data_dicttree_purge)
1457
+ @ddt.unpack
1458
+ def test_dicttree_purge(self, tree: dicttree[str, int], expect):
1459
+ self.assertEqual(expect, dicttree_purge(tree))
1460
+
1461
+ data_dicttree_add = [
1462
+ ({}, ["a"], 1, True, False, {"a": (1, {})}),
1463
+ ({"a": (1, {})}, ["b"], 2, True, False, {"a": (1, {}), "b": (2, {})}),
1464
+ ({"a": (1, {})}, ["a"], 2, True, True, {"a": (2, {})}),
1465
+ (
1466
+ {
1467
+ "a": (1, {}),
1468
+ "b": (2, {"c": (3, {})}),
1469
+ "d": (None, {"e": (5, {})}),
1470
+ },
1471
+ ["b", "d", "e"],
1472
+ 5,
1473
+ True,
1474
+ False,
1475
+ {
1476
+ "a": (1, {}),
1477
+ "b": (2, {"c": (3, {}), "d": (None, {"e": (5, {})})}),
1478
+ "d": (None, {"e": (5, {})}),
1479
+ },
1480
+ ),
1481
+ (
1482
+ {
1483
+ "a": (1, {}),
1484
+ "b": (2, {"c": (3, {})}),
1485
+ "d": (None, {"e": (5, {})}),
1486
+ },
1487
+ ["d"],
1488
+ 4,
1489
+ True,
1490
+ False,
1491
+ {
1492
+ "a": (1, {}),
1493
+ "b": (2, {"c": (3, {})}),
1494
+ "d": (4, {"e": (5, {})}),
1495
+ },
1496
+ ),
1497
+ (
1498
+ {
1499
+ "a": (1, {}),
1500
+ "b": (2, {"c": (3, {})}),
1501
+ "d": (None, {"e": (5, {})}),
1502
+ },
1503
+ ["b"],
1504
+ 22,
1505
+ True,
1506
+ True,
1507
+ {
1508
+ "a": (1, {}),
1509
+ "b": (22, {"c": (3, {})}),
1510
+ "d": (None, {"e": (5, {})}),
1511
+ },
1512
+ ),
1513
+ (
1514
+ {
1515
+ "a": (1, {}),
1516
+ "b": (2, {"c": (3, {})}),
1517
+ "d": (None, {"e": (5, {})}),
1518
+ },
1519
+ ["d", "e"],
1520
+ 55,
1521
+ True,
1522
+ True,
1523
+ {
1524
+ "a": (1, {}),
1525
+ "b": (2, {"c": (3, {})}),
1526
+ "d": (None, {"e": (55, {})}),
1527
+ },
1528
+ ),
1529
+ ]
1530
+
1531
+ @ddt.idata(data_dicttree_add)
1532
+ @ddt.unpack
1533
+ def test_dicttree_add(self, tree: dicttree[str, int], path, value, create_prefix, overwrite, expect):
1534
+ self.assertEqual(expect, dicttree_add(tree, path, value, create_prefix=create_prefix, overwrite=overwrite))
1535
+
1536
+ data_dicttree_add__bad_cases = [
1537
+ ({}, [], 0, True, False),
1538
+ ({"a": (1, {})}, ["a"], 2, True, False),
1539
+ (
1540
+ {
1541
+ "a": (1, {}),
1542
+ "b": (2, {"c": (3, {})}),
1543
+ "d": (None, {"e": (5, {})}),
1544
+ },
1545
+ ["b", "d", "e"],
1546
+ 5,
1547
+ False,
1548
+ False,
1549
+ ),
1550
+ ]
1551
+
1552
+ @ddt.idata(data_dicttree_add__bad_cases)
1553
+ @ddt.unpack
1554
+ def test_dicttree_add__bad_cases(self, tree: dicttree[str, int], path, value, create_prefix, overwrite):
1555
+ with self.assertRaises(ValueError):
1556
+ dicttree_add(tree, path, value, create_prefix=create_prefix, overwrite=overwrite)
1557
+
1558
+ data_dicttree_remove = [
1559
+ ({"a": (1, {})}, ["a"], False, {"a": (None, {})}),
1560
+ (
1561
+ {
1562
+ "a": (1, {}),
1563
+ "b": (2, {"c": (3, {})}),
1564
+ "d": (None, {"e": (5, {})}),
1565
+ },
1566
+ ["b"],
1567
+ False,
1568
+ {
1569
+ "a": (1, {}),
1570
+ "b": (None, {"c": (3, {})}),
1571
+ "d": (None, {"e": (5, {})}),
1572
+ },
1573
+ ),
1574
+ (
1575
+ {
1576
+ "a": (1, {}),
1577
+ "b": (2, {"c": (3, {})}),
1578
+ "d": (None, {"e": (5, {})}),
1579
+ },
1580
+ ["b"],
1581
+ True,
1582
+ {
1583
+ "a": (1, {}),
1584
+ "b": (None, {}),
1585
+ "d": (None, {"e": (5, {})}),
1586
+ },
1587
+ ),
1588
+ (
1589
+ {
1590
+ "a": (1, {}),
1591
+ "b": (2, {"c": (3, {})}),
1592
+ "d": (None, {"e": (5, {})}),
1593
+ },
1594
+ ["b", "c"],
1595
+ False,
1596
+ {
1597
+ "a": (1, {}),
1598
+ "b": (2, {"c": (None, {})}),
1599
+ "d": (None, {"e": (5, {})}),
1600
+ },
1601
+ ),
1602
+ ]
1603
+
1604
+ @ddt.idata(data_dicttree_remove)
1605
+ @ddt.unpack
1606
+ def test_dicttree_remove(self, tree: dicttree[str, int], path, recursive, expect):
1607
+ self.assertEqual(expect, dicttree_remove(tree, path, recursive=recursive))
1608
+
1609
+ data_dicttree_remove__bad_cases = [
1610
+ ({}, [], False),
1611
+ ({"a": (1, {})}, ["b"], False),
1612
+ (
1613
+ {
1614
+ "a": (1, {}),
1615
+ "b": (2, {"c": (3, {})}),
1616
+ "d": (None, {"e": (5, {})}),
1617
+ },
1618
+ ["e", "f"],
1619
+ False,
1620
+ ),
1621
+ ]
1622
+
1623
+ @ddt.idata(data_dicttree_remove__bad_cases)
1624
+ @ddt.unpack
1625
+ def test_dicttree_remove__bad_cases(self, tree: dicttree[str, int], path, recursive):
1626
+ with self.assertRaises(ValueError):
1627
+ dicttree_remove(tree, path, recursive=recursive)
1628
+
1629
+
940
1630
  @dataclasses.dataclass(frozen=True, eq=True, order=True)
941
1631
  class Naming(object):
942
1632
  serial: int
@@ -3,7 +3,7 @@ import unittest
3
3
 
4
4
  import ddt
5
5
 
6
- from iker.common.utils.sequtils import last
6
+ from iker.common.utils.iterutils import last
7
7
  from iker.common.utils.span import SpanRelation
8
8
  from iker.common.utils.span import span_relation, spans_intersect, spans_subtract, spans_union
9
9