mixinv2 0.3.0.post57.dev0__tar.gz → 0.3.0.post58.dev0__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.
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/PKG-INFO +1 -1
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/src/mixinv2/__init__.py +4 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/src/mixinv2/_core.py +374 -14
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/src/mixinv2/_runtime.py +48 -29
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/.gitignore +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/README.md +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/docs/Makefile +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/docs/_static/favicon.svg +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/docs/_static/logo.svg +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/docs/conf.py +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/docs/index.rst +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/docs/installation.rst +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/docs/mixinv2-tutorial.rst +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/docs/specification.md +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/docs/tutorial.rst +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/pyproject.toml +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/src/mixinv2/_config.py +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/src/mixinv2/_interned_linked_list.py +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/src/mixinv2/_mixin_directory.py +0 -0
- {mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/src/mixinv2/_mixin_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mixinv2
|
|
3
|
-
Version: 0.3.0.
|
|
3
|
+
Version: 0.3.0.post58.dev0
|
|
4
4
|
Summary: A dependency injection framework with pytest-fixture syntax, plus a configuration language for declarative programming
|
|
5
5
|
Project-URL: Repository, https://github.com/Atry/MIXINv2
|
|
6
6
|
Author-email: "Yang, Bo" <yang-bo@yang-bo.com>
|
|
@@ -18,6 +18,9 @@ Decorators:
|
|
|
18
18
|
Runtime:
|
|
19
19
|
- :func:`evaluate`
|
|
20
20
|
|
|
21
|
+
Exceptions:
|
|
22
|
+
- :class:`Bottom`
|
|
23
|
+
|
|
21
24
|
Reference types (parameters to :func:`extend`):
|
|
22
25
|
- :class:`AbsoluteReference`
|
|
23
26
|
- :class:`RelativeReference`
|
|
@@ -40,6 +43,7 @@ from mixinv2._core import patch_many as patch_many
|
|
|
40
43
|
from mixinv2._core import public as public
|
|
41
44
|
from mixinv2._core import resource as resource
|
|
42
45
|
from mixinv2._core import scope as scope
|
|
46
|
+
from mixinv2._core import Bottom as Bottom
|
|
43
47
|
from mixinv2._runtime import evaluate as evaluate
|
|
44
48
|
|
|
45
49
|
if TYPE_CHECKING:
|
|
@@ -530,6 +530,7 @@ from __future__ import annotations
|
|
|
530
530
|
|
|
531
531
|
from abc import ABC, abstractmethod
|
|
532
532
|
from collections import defaultdict, deque
|
|
533
|
+
from contextvars import ContextVar
|
|
533
534
|
from dataclasses import dataclass, field, replace
|
|
534
535
|
from enum import Enum, auto
|
|
535
536
|
from functools import cached_property
|
|
@@ -603,6 +604,334 @@ TResult_co = TypeVar("TResult_co", covariant=True)
|
|
|
603
604
|
P = ParamSpec("P")
|
|
604
605
|
|
|
605
606
|
|
|
607
|
+
class _FixpointContext:
|
|
608
|
+
"""Tracks the state of a fixpoint iteration (digest cycle).
|
|
609
|
+
|
|
610
|
+
Stored in a ContextVar so that nested/concurrent fixpoint computations
|
|
611
|
+
are isolated per-thread/per-coroutine.
|
|
612
|
+
"""
|
|
613
|
+
|
|
614
|
+
__slots__ = ("computing", "reentrant", "participant_ids", "participant_refs",
|
|
615
|
+
"_clearable_attr_names", "approximations")
|
|
616
|
+
|
|
617
|
+
def __init__(self, clearable_attr_names: frozenset[str]) -> None:
|
|
618
|
+
self.computing: set[tuple[int, str]] = set()
|
|
619
|
+
self.reentrant: bool = False
|
|
620
|
+
self.participant_ids: set[int] = set()
|
|
621
|
+
self.participant_refs: list[object] = []
|
|
622
|
+
self._clearable_attr_names = clearable_attr_names
|
|
623
|
+
self.approximations: dict[tuple[int, str], object] = {}
|
|
624
|
+
|
|
625
|
+
def add_participant(self, instance: object) -> None:
|
|
626
|
+
instance_id = id(instance)
|
|
627
|
+
if instance_id not in self.participant_ids:
|
|
628
|
+
self.participant_ids.add(instance_id)
|
|
629
|
+
self.participant_refs.append(instance)
|
|
630
|
+
|
|
631
|
+
def clear_participant_caches(self) -> None:
|
|
632
|
+
"""Clear all fixpoint-related cached values on all participants.
|
|
633
|
+
|
|
634
|
+
Before clearing, save each value into ``approximations`` so that
|
|
635
|
+
intermediate fixpoint_cached_property computations can use their
|
|
636
|
+
previous iteration's result as an approximation instead of bottom
|
|
637
|
+
when they encounter reentry.
|
|
638
|
+
"""
|
|
639
|
+
for instance in self.participant_refs:
|
|
640
|
+
instance_dict = instance.__dict__
|
|
641
|
+
instance_id = id(instance)
|
|
642
|
+
for attr_name in self._clearable_attr_names:
|
|
643
|
+
value = instance_dict.pop(attr_name, None)
|
|
644
|
+
if value is not None:
|
|
645
|
+
self.approximations[(instance_id, attr_name)] = value
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
_fixpoint_context_var: ContextVar[_FixpointContext | None] = ContextVar(
|
|
649
|
+
"_fixpoint_context_var", default=None
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
class Bottom(RecursionError):
|
|
654
|
+
"""Raised when fixpoint iteration is exhausted or reentry is detected with no iterations remaining.
|
|
655
|
+
|
|
656
|
+
Carries the best approximation computed so far in ``incomplete_result``.
|
|
657
|
+
As a ``RecursionError`` subclass, existing code that catches ``RecursionError``
|
|
658
|
+
will also catch ``Bottom``.
|
|
659
|
+
"""
|
|
660
|
+
|
|
661
|
+
incomplete_result: object
|
|
662
|
+
|
|
663
|
+
def __init__(self, message: str, *, incomplete_result: object) -> None:
|
|
664
|
+
super().__init__(message)
|
|
665
|
+
self.incomplete_result = incomplete_result
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
_max_fixpoint_iterations_var: ContextVar[int] = ContextVar(
|
|
669
|
+
"_max_fixpoint_iterations_var", default=100
|
|
670
|
+
)
|
|
671
|
+
_FIXPOINT_SENTINEL = object()
|
|
672
|
+
|
|
673
|
+
# Registry of attribute names that need clearing during fixpoint digest cycles.
|
|
674
|
+
# Populated by fixpoint_cached_property and fixpoint_dependent decorators.
|
|
675
|
+
_fixpoint_clearable_attrs: set[str] = set()
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _accumulate_defaultdict_set(
|
|
679
|
+
accumulator: defaultdict[object, set[object]],
|
|
680
|
+
new_result: defaultdict[object, set[object]],
|
|
681
|
+
) -> bool:
|
|
682
|
+
"""Merge new_result into accumulator (pointwise set union).
|
|
683
|
+
|
|
684
|
+
Returns True if accumulator grew (new entries were added).
|
|
685
|
+
"""
|
|
686
|
+
changed = False
|
|
687
|
+
for key, values in new_result.items():
|
|
688
|
+
existing = accumulator[key]
|
|
689
|
+
old_size = len(existing)
|
|
690
|
+
existing.update(values)
|
|
691
|
+
if len(existing) > old_size:
|
|
692
|
+
changed = True
|
|
693
|
+
return changed
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class fixpoint_cached_property:
|
|
697
|
+
"""A cached_property that supports mutual-recursion via least fixpoint iteration.
|
|
698
|
+
|
|
699
|
+
API-compatible with functools.cached_property. When reentry is detected
|
|
700
|
+
(mutual recursion), returns the previous iteration's approximation
|
|
701
|
+
(or ``bottom()`` on the first iteration). The outermost caller drives
|
|
702
|
+
a digest loop until values stabilize (no reentry occurs in a round).
|
|
703
|
+
|
|
704
|
+
Usage::
|
|
705
|
+
|
|
706
|
+
@fixpoint_cached_property(bottom=lambda: defaultdict(set))
|
|
707
|
+
def qualified_this(self):
|
|
708
|
+
...
|
|
709
|
+
"""
|
|
710
|
+
|
|
711
|
+
def __init__(
|
|
712
|
+
self,
|
|
713
|
+
func: Callable = None,
|
|
714
|
+
*,
|
|
715
|
+
bottom: Callable[[], object],
|
|
716
|
+
accumulate: Callable[[object, object], bool] | None = None,
|
|
717
|
+
) -> None:
|
|
718
|
+
# Support both @fixpoint_cached_property(bottom=...) and direct call
|
|
719
|
+
self._bottom = bottom
|
|
720
|
+
self._accumulate = accumulate
|
|
721
|
+
if func is not None:
|
|
722
|
+
self.func: Callable = func
|
|
723
|
+
self.attrname: str = func.__name__
|
|
724
|
+
self.__doc__ = func.__doc__
|
|
725
|
+
_fixpoint_clearable_attrs.add(self.attrname)
|
|
726
|
+
|
|
727
|
+
def __call__(self, func: Callable) -> "fixpoint_cached_property":
|
|
728
|
+
"""Support @fixpoint_cached_property(bottom=...) decorator syntax."""
|
|
729
|
+
self.func = func
|
|
730
|
+
self.attrname = func.__name__
|
|
731
|
+
self.__doc__ = func.__doc__
|
|
732
|
+
_fixpoint_clearable_attrs.add(self.attrname)
|
|
733
|
+
return self
|
|
734
|
+
|
|
735
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
736
|
+
if not hasattr(self, "attrname"):
|
|
737
|
+
self.attrname = name
|
|
738
|
+
_fixpoint_clearable_attrs.add(self.attrname)
|
|
739
|
+
|
|
740
|
+
def __get__(self, instance: object, owner: type = None) -> object:
|
|
741
|
+
if instance is None:
|
|
742
|
+
return self
|
|
743
|
+
|
|
744
|
+
# Fast path: already cached
|
|
745
|
+
cache = instance.__dict__
|
|
746
|
+
value = cache.get(self.attrname)
|
|
747
|
+
if value is not None:
|
|
748
|
+
max_iterations = _max_fixpoint_iterations_var.get()
|
|
749
|
+
if max_iterations == 0:
|
|
750
|
+
return value
|
|
751
|
+
# Detect reentry: if this key is currently being computed
|
|
752
|
+
# (on the call stack), accessing its cached approximation
|
|
753
|
+
# means the fixpoint has not converged yet.
|
|
754
|
+
context = _fixpoint_context_var.get()
|
|
755
|
+
if context is not None:
|
|
756
|
+
key = (id(instance), self.attrname)
|
|
757
|
+
if key in context.computing:
|
|
758
|
+
context.reentrant = True
|
|
759
|
+
context.add_participant(instance)
|
|
760
|
+
return value
|
|
761
|
+
|
|
762
|
+
max_iterations = _max_fixpoint_iterations_var.get()
|
|
763
|
+
context = _fixpoint_context_var.get()
|
|
764
|
+
instance_id = id(instance)
|
|
765
|
+
key = (instance_id, self.attrname)
|
|
766
|
+
|
|
767
|
+
if context is None:
|
|
768
|
+
# I am the driver — start a digest loop (or single-pass for max_iterations=0)
|
|
769
|
+
context = _FixpointContext(
|
|
770
|
+
clearable_attr_names=frozenset(_fixpoint_clearable_attrs)
|
|
771
|
+
)
|
|
772
|
+
token = _fixpoint_context_var.set(context)
|
|
773
|
+
try:
|
|
774
|
+
if max_iterations == 0:
|
|
775
|
+
# Zero-iteration mode: compute once with reentry detection.
|
|
776
|
+
# Reentry raises Bottom instead of infinite recursion.
|
|
777
|
+
context.computing.add(key)
|
|
778
|
+
result = self.func(instance)
|
|
779
|
+
cache[self.attrname] = result
|
|
780
|
+
return result
|
|
781
|
+
|
|
782
|
+
approximation = self._bottom()
|
|
783
|
+
accumulator = self._bottom() if self._accumulate is not None else None
|
|
784
|
+
previous_result = _FIXPOINT_SENTINEL
|
|
785
|
+
for iteration in range(max_iterations):
|
|
786
|
+
context.computing.add(key)
|
|
787
|
+
context.add_participant(instance)
|
|
788
|
+
result = self.func(instance)
|
|
789
|
+
|
|
790
|
+
if not context.reentrant:
|
|
791
|
+
# No reentry this round — fixpoint reached
|
|
792
|
+
cache[self.attrname] = result
|
|
793
|
+
return result
|
|
794
|
+
|
|
795
|
+
if self._accumulate is not None:
|
|
796
|
+
# Monotonic accumulation: merge each iteration's
|
|
797
|
+
# result into an accumulator that only grows.
|
|
798
|
+
# This prevents oscillation when intermediate
|
|
799
|
+
# computations encounter cycles in varying order.
|
|
800
|
+
changed = self._accumulate(accumulator, result)
|
|
801
|
+
if not changed and iteration > 0:
|
|
802
|
+
cache[self.attrname] = accumulator
|
|
803
|
+
return accumulator
|
|
804
|
+
# Use the accumulator as next round's approximation
|
|
805
|
+
approximation = accumulator
|
|
806
|
+
else:
|
|
807
|
+
# Exact equality convergence (original behavior)
|
|
808
|
+
if result == previous_result:
|
|
809
|
+
cache[self.attrname] = result
|
|
810
|
+
return result
|
|
811
|
+
previous_result = result
|
|
812
|
+
approximation = result
|
|
813
|
+
|
|
814
|
+
# Cache current approximation, clear all intermediate
|
|
815
|
+
# caches, and re-run
|
|
816
|
+
cache[self.attrname] = approximation
|
|
817
|
+
context.clear_participant_caches()
|
|
818
|
+
# Restore driver's own approximation
|
|
819
|
+
cache[self.attrname] = approximation
|
|
820
|
+
context.computing.clear()
|
|
821
|
+
context.reentrant = False
|
|
822
|
+
|
|
823
|
+
raise Bottom(
|
|
824
|
+
f"fixpoint_cached_property '{self.attrname}' did not converge "
|
|
825
|
+
f"after {max_iterations} iterations",
|
|
826
|
+
incomplete_result=approximation,
|
|
827
|
+
)
|
|
828
|
+
finally:
|
|
829
|
+
_fixpoint_context_var.reset(token)
|
|
830
|
+
elif key in context.computing:
|
|
831
|
+
# Reentry detected — return previous approximation or bottom.
|
|
832
|
+
context.reentrant = True
|
|
833
|
+
context.add_participant(instance)
|
|
834
|
+
if max_iterations == 0:
|
|
835
|
+
raise Bottom(
|
|
836
|
+
f"fixpoint_cached_property '{self.attrname}': "
|
|
837
|
+
f"reentry detected with max_fixpoint_iterations=0",
|
|
838
|
+
incomplete_result=self._bottom(),
|
|
839
|
+
)
|
|
840
|
+
# Check the instance cache first, then fall back to saved
|
|
841
|
+
# approximations from the previous iteration.
|
|
842
|
+
approximation = cache.get(self.attrname)
|
|
843
|
+
if approximation is not None:
|
|
844
|
+
return approximation
|
|
845
|
+
saved = context.approximations.get(key)
|
|
846
|
+
if saved is not None:
|
|
847
|
+
return saved
|
|
848
|
+
return self._bottom()
|
|
849
|
+
else:
|
|
850
|
+
# Inside a fixpoint context but this is a fresh (instance, attr)
|
|
851
|
+
# pair — compute normally. Keep the key in ``computing`` only
|
|
852
|
+
# while ``self.func`` runs so that cycles through this key are
|
|
853
|
+
# detected by the ``elif`` branch above. Once computation
|
|
854
|
+
# finishes, remove the key so the fast-path cache check does
|
|
855
|
+
# not misidentify a later read of this cached value as reentry.
|
|
856
|
+
context.computing.add(key)
|
|
857
|
+
context.add_participant(instance)
|
|
858
|
+
result = self.func(instance)
|
|
859
|
+
context.computing.discard(key)
|
|
860
|
+
cache[self.attrname] = result
|
|
861
|
+
return result
|
|
862
|
+
|
|
863
|
+
def __set__(self, instance: object, value: object) -> None:
|
|
864
|
+
"""Data descriptor setter to ensure __get__ is always called."""
|
|
865
|
+
instance.__dict__[self.attrname] = value
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
class _fixpoint_dependent_property:
|
|
869
|
+
"""A cached_property that registers its instance as a fixpoint participant.
|
|
870
|
+
|
|
871
|
+
Behaves like ``functools.cached_property`` but, when computed inside an
|
|
872
|
+
active fixpoint context, registers the instance so that
|
|
873
|
+
``clear_participant_caches`` will clear the cached value between
|
|
874
|
+
iterations. Without this, stale values computed from an incomplete
|
|
875
|
+
fixpoint approximation survive across iterations.
|
|
876
|
+
"""
|
|
877
|
+
|
|
878
|
+
def __init__(self, func: Callable) -> None:
|
|
879
|
+
self.func = func
|
|
880
|
+
self.attrname = func.__name__
|
|
881
|
+
self.__doc__ = func.__doc__
|
|
882
|
+
_fixpoint_clearable_attrs.add(self.attrname)
|
|
883
|
+
|
|
884
|
+
def __set_name__(self, owner: type, name: str) -> None:
|
|
885
|
+
if not hasattr(self, "attrname"):
|
|
886
|
+
self.attrname = name
|
|
887
|
+
_fixpoint_clearable_attrs.add(self.attrname)
|
|
888
|
+
|
|
889
|
+
def __get__(self, instance: object, owner: type = None) -> object:
|
|
890
|
+
if instance is None:
|
|
891
|
+
return self
|
|
892
|
+
|
|
893
|
+
cache = instance.__dict__
|
|
894
|
+
value = cache.get(self.attrname)
|
|
895
|
+
if value is not None:
|
|
896
|
+
return value
|
|
897
|
+
|
|
898
|
+
if _max_fixpoint_iterations_var.get() > 0:
|
|
899
|
+
# Register as participant so clear_participant_caches can
|
|
900
|
+
# invalidate this cached value between fixpoint iterations.
|
|
901
|
+
context = _fixpoint_context_var.get()
|
|
902
|
+
if context is not None:
|
|
903
|
+
context.add_participant(instance)
|
|
904
|
+
|
|
905
|
+
value = self.func(instance)
|
|
906
|
+
cache[self.attrname] = value
|
|
907
|
+
return value
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def fixpoint_dependent(func: Callable) -> _fixpoint_dependent_property:
|
|
911
|
+
"""Mark a cached_property as dependent on fixpoint_cached_property values.
|
|
912
|
+
|
|
913
|
+
During fixpoint digest cycles, these caches are cleared between iterations
|
|
914
|
+
so they are recomputed with updated approximations.
|
|
915
|
+
|
|
916
|
+
Usage::
|
|
917
|
+
|
|
918
|
+
@fixpoint_dependent
|
|
919
|
+
@cached_property
|
|
920
|
+
def symbol_kind(self):
|
|
921
|
+
...
|
|
922
|
+
|
|
923
|
+
Or equivalently::
|
|
924
|
+
|
|
925
|
+
@fixpoint_dependent
|
|
926
|
+
def symbol_kind(self):
|
|
927
|
+
...
|
|
928
|
+
"""
|
|
929
|
+
if isinstance(func, cached_property):
|
|
930
|
+
return _fixpoint_dependent_property(func.func)
|
|
931
|
+
else:
|
|
932
|
+
return _fixpoint_dependent_property(func)
|
|
933
|
+
|
|
934
|
+
|
|
606
935
|
class HasDict:
|
|
607
936
|
"""
|
|
608
937
|
Extendable helper class that adds ``__dict__`` slot for classes that need ``@cached_property``.
|
|
@@ -892,7 +1221,7 @@ class MixinSymbol(HasDict, Mapping[Hashable, "MixinSymbol"], Symbol):
|
|
|
892
1221
|
return tuple(definitions)
|
|
893
1222
|
|
|
894
1223
|
@final
|
|
895
|
-
@
|
|
1224
|
+
@fixpoint_dependent
|
|
896
1225
|
def normalized_references(self) -> tuple["ResolvedReference", ...]:
|
|
897
1226
|
"""
|
|
898
1227
|
Flatten all bases from all definitions into a single tuple.
|
|
@@ -1051,9 +1380,14 @@ class MixinSymbol(HasDict, Mapping[Hashable, "MixinSymbol"], Symbol):
|
|
|
1051
1380
|
# Use Nested to create child symbol with lazy definition resolution
|
|
1052
1381
|
compiled_symbol = MixinSymbol(origin=Nested(outer=self, key=key))
|
|
1053
1382
|
|
|
1054
|
-
# Check if any super union of self has this key as an own key
|
|
1055
|
-
|
|
1056
|
-
|
|
1383
|
+
# Check if self or any super union of self has this key as an own key.
|
|
1384
|
+
# During fixpoint iteration, qualified_this may not have converged yet,
|
|
1385
|
+
# so skip this validation to avoid false negatives.
|
|
1386
|
+
if _fixpoint_context_var.get() is None:
|
|
1387
|
+
if not self.has_own_key(key) and not any(
|
|
1388
|
+
super_union.has_own_key(key) for super_union in self.qualified_this
|
|
1389
|
+
):
|
|
1390
|
+
raise KeyError(key)
|
|
1057
1391
|
|
|
1058
1392
|
self._nested[key] = compiled_symbol
|
|
1059
1393
|
return compiled_symbol
|
|
@@ -1072,16 +1406,19 @@ class MixinSymbol(HasDict, Mapping[Hashable, "MixinSymbol"], Symbol):
|
|
|
1072
1406
|
case MixinSymbol() as outer_symbol:
|
|
1073
1407
|
return outer_symbol.depth + 1
|
|
1074
1408
|
|
|
1075
|
-
@
|
|
1409
|
+
@fixpoint_dependent
|
|
1076
1410
|
def is_public(self):
|
|
1077
|
-
# Check
|
|
1411
|
+
# Check own definitions first (does not depend on qualified_this)
|
|
1412
|
+
if any(definition.is_public for definition in self.definitions):
|
|
1413
|
+
return True
|
|
1414
|
+
# Check inherited definitions via qualified_this
|
|
1078
1415
|
return any(
|
|
1079
1416
|
definition.is_public
|
|
1080
1417
|
for super_symbol in self.qualified_this
|
|
1081
1418
|
for definition in super_symbol.definitions
|
|
1082
1419
|
)
|
|
1083
1420
|
|
|
1084
|
-
@
|
|
1421
|
+
@fixpoint_dependent
|
|
1085
1422
|
def is_eager(self):
|
|
1086
1423
|
return any(
|
|
1087
1424
|
definition.is_eager
|
|
@@ -1090,7 +1427,7 @@ class MixinSymbol(HasDict, Mapping[Hashable, "MixinSymbol"], Symbol):
|
|
|
1090
1427
|
if isinstance(definition, MergerDefinition)
|
|
1091
1428
|
)
|
|
1092
1429
|
|
|
1093
|
-
@
|
|
1430
|
+
@fixpoint_dependent
|
|
1094
1431
|
def symbol_kind(self) -> "SymbolKind":
|
|
1095
1432
|
"""Classify this symbol into one of three categories.
|
|
1096
1433
|
|
|
@@ -1176,7 +1513,10 @@ class MixinSymbol(HasDict, Mapping[Hashable, "MixinSymbol"], Symbol):
|
|
|
1176
1513
|
case _:
|
|
1177
1514
|
raise ValueError("Multiple pure merger definitions found")
|
|
1178
1515
|
|
|
1179
|
-
@
|
|
1516
|
+
@fixpoint_cached_property(
|
|
1517
|
+
bottom=lambda: defaultdict(set),
|
|
1518
|
+
accumulate=_accumulate_defaultdict_set,
|
|
1519
|
+
)
|
|
1180
1520
|
def qualified_this(self) -> Mapping["MixinSymbol", Collection[MixinSymbol]]:
|
|
1181
1521
|
"""Map each overlay of ``self`` to the outer scopes that instantiate it.
|
|
1182
1522
|
|
|
@@ -1220,7 +1560,7 @@ class MixinSymbol(HasDict, Mapping[Hashable, "MixinSymbol"], Symbol):
|
|
|
1220
1560
|
if outer_union.has_own_key(key):
|
|
1221
1561
|
yield outer_union[key]
|
|
1222
1562
|
|
|
1223
|
-
@
|
|
1563
|
+
@fixpoint_dependent
|
|
1224
1564
|
def overlays(self):
|
|
1225
1565
|
return frozenset(self._generate_overlays())
|
|
1226
1566
|
|
|
@@ -2475,9 +2815,15 @@ class AbsoluteReference:
|
|
|
2475
2815
|
for part in self.path:
|
|
2476
2816
|
child_symbol = current_symbol.get(part)
|
|
2477
2817
|
if child_symbol is None:
|
|
2818
|
+
kind_hint = (
|
|
2819
|
+
f" (unexpected kind {current_symbol.symbol_kind.name})"
|
|
2820
|
+
if current_symbol.symbol_kind is not SymbolKind.SCOPE
|
|
2821
|
+
else ""
|
|
2822
|
+
)
|
|
2478
2823
|
raise ValueError(
|
|
2479
2824
|
f"Cannot navigate path {self.path!r}: "
|
|
2480
|
-
f"
|
|
2825
|
+
f"{current_symbol.path}{kind_hint}"
|
|
2826
|
+
f" has no child '{part}'"
|
|
2481
2827
|
)
|
|
2482
2828
|
current_symbol = child_symbol
|
|
2483
2829
|
|
|
@@ -2533,7 +2879,9 @@ class RelativeReference:
|
|
|
2533
2879
|
if child_symbol is None:
|
|
2534
2880
|
raise ValueError(
|
|
2535
2881
|
f"Cannot navigate path {self.path!r}: "
|
|
2536
|
-
f"
|
|
2882
|
+
f"{current_symbol.path} (unexpected kind"
|
|
2883
|
+
f" {current_symbol.symbol_kind.name})"
|
|
2884
|
+
f" has no child '{part}'"
|
|
2537
2885
|
)
|
|
2538
2886
|
current_symbol = child_symbol
|
|
2539
2887
|
|
|
@@ -2738,9 +3086,15 @@ class LexicalReference:
|
|
|
2738
3086
|
for part in hashable_path:
|
|
2739
3087
|
child_symbol = current_symbol.get(part)
|
|
2740
3088
|
if child_symbol is None:
|
|
3089
|
+
kind_hint = (
|
|
3090
|
+
f" (unexpected kind {current_symbol.symbol_kind.name})"
|
|
3091
|
+
if current_symbol.symbol_kind is not SymbolKind.SCOPE
|
|
3092
|
+
else ""
|
|
3093
|
+
)
|
|
2741
3094
|
raise ValueError(
|
|
2742
3095
|
f"Cannot navigate path {hashable_path!r}: "
|
|
2743
|
-
f"
|
|
3096
|
+
f"{current_symbol.path}{kind_hint}"
|
|
3097
|
+
f" has no child '{part}'"
|
|
2744
3098
|
)
|
|
2745
3099
|
current_symbol = child_symbol
|
|
2746
3100
|
|
|
@@ -2828,9 +3182,15 @@ class QualifiedThisReference:
|
|
|
2828
3182
|
for part in hashable_path:
|
|
2829
3183
|
child_symbol = current_symbol.get(part)
|
|
2830
3184
|
if child_symbol is None:
|
|
3185
|
+
kind_hint = (
|
|
3186
|
+
f" (unexpected kind {current_symbol.symbol_kind.name})"
|
|
3187
|
+
if current_symbol.symbol_kind is not SymbolKind.SCOPE
|
|
3188
|
+
else ""
|
|
3189
|
+
)
|
|
2831
3190
|
raise ValueError(
|
|
2832
3191
|
f"Cannot navigate path {hashable_path!r}: "
|
|
2833
|
-
f"
|
|
3192
|
+
f"{current_symbol.path}{kind_hint}"
|
|
3193
|
+
f" has no child '{part}'"
|
|
2834
3194
|
)
|
|
2835
3195
|
current_symbol = child_symbol
|
|
2836
3196
|
|
|
@@ -30,7 +30,12 @@ from typing import (
|
|
|
30
30
|
final,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
-
from mixinv2._core import
|
|
33
|
+
from mixinv2._core import (
|
|
34
|
+
HasDict,
|
|
35
|
+
OuterSentinel,
|
|
36
|
+
SymbolKind,
|
|
37
|
+
_max_fixpoint_iterations_var,
|
|
38
|
+
)
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
class KwargsSentinel(Enum):
|
|
@@ -634,6 +639,7 @@ class MultiplePatcher(Patcher[TPatch_co]):
|
|
|
634
639
|
def evaluate(
|
|
635
640
|
*namespaces: "ModuleType | ScopeDefinition",
|
|
636
641
|
modules_public: bool = False,
|
|
642
|
+
max_fixpoint_iterations: int = 100,
|
|
637
643
|
) -> Scope:
|
|
638
644
|
"""
|
|
639
645
|
Resolves a Scope from the given namespaces.
|
|
@@ -650,6 +656,10 @@ def evaluate(
|
|
|
650
656
|
:param namespaces: Modules or namespace definitions (decorated with @scope) to resolve.
|
|
651
657
|
:param modules_public: If True, modules are marked as public, making their submodules
|
|
652
658
|
accessible via attribute access. Defaults to False (private by default).
|
|
659
|
+
:param max_fixpoint_iterations: Maximum number of fixpoint iterations for
|
|
660
|
+
resolving cyclic dependencies. ``0`` disables fixpoint iteration and
|
|
661
|
+
raises ``Bottom`` on reentry. Default ``100`` iterates until convergence
|
|
662
|
+
or raises ``Bottom`` if not converged.
|
|
653
663
|
:return: The root Scope.
|
|
654
664
|
|
|
655
665
|
Example::
|
|
@@ -657,6 +667,7 @@ def evaluate(
|
|
|
657
667
|
root = evaluate(MyNamespace)
|
|
658
668
|
root = evaluate(Base, Override) # Union mount
|
|
659
669
|
root = evaluate(my_package, modules_public=True) # Make modules accessible
|
|
670
|
+
root = evaluate(MyNamespace, max_fixpoint_iterations=0) # No fixpoint iteration
|
|
660
671
|
|
|
661
672
|
"""
|
|
662
673
|
from dataclasses import replace
|
|
@@ -671,36 +682,44 @@ def evaluate(
|
|
|
671
682
|
)
|
|
672
683
|
|
|
673
684
|
assert namespaces, "evaluate() requires at least one namespace"
|
|
685
|
+
if max_fixpoint_iterations < 0:
|
|
686
|
+
raise ValueError(
|
|
687
|
+
f"max_fixpoint_iterations must be non-negative, got {max_fixpoint_iterations}"
|
|
688
|
+
)
|
|
674
689
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
690
|
+
token = _max_fixpoint_iterations_var.set(max_fixpoint_iterations)
|
|
691
|
+
try:
|
|
692
|
+
def to_scope_definition(
|
|
693
|
+
namespace: ModuleType | ScopeDefinition,
|
|
694
|
+
) -> ScopeDefinition:
|
|
695
|
+
if isinstance(namespace, ScopeDefinition):
|
|
696
|
+
return namespace
|
|
697
|
+
if isinstance(namespace, ModuleType):
|
|
698
|
+
definition = _parse_package(namespace)
|
|
699
|
+
if modules_public:
|
|
700
|
+
return replace(definition, is_public=True)
|
|
701
|
+
return definition
|
|
702
|
+
assert_never(namespace)
|
|
703
|
+
|
|
704
|
+
definitions = tuple(to_scope_definition(namespace) for namespace in namespaces)
|
|
705
|
+
|
|
706
|
+
root_symbol = MixinSymbol(origin=definitions)
|
|
707
|
+
|
|
708
|
+
# Create a synthetic root Mixin to enable lexical scope navigation
|
|
709
|
+
# This is needed so that children of the root scope can navigate up
|
|
710
|
+
# to find parent scope dependencies (via get_mixin)
|
|
711
|
+
root_mixin = Mixin(
|
|
712
|
+
symbol=root_symbol,
|
|
713
|
+
outer=OuterSentinel.ROOT,
|
|
714
|
+
kwargs=KwargsSentinel.STATIC, # Root is always static
|
|
715
|
+
)
|
|
699
716
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
717
|
+
# Evaluate the root mixin to get the Scope
|
|
718
|
+
result = root_mixin.evaluated
|
|
719
|
+
assert isinstance(result, Scope)
|
|
720
|
+
return result
|
|
721
|
+
finally:
|
|
722
|
+
_max_fixpoint_iterations_var.reset(token)
|
|
704
723
|
|
|
705
724
|
|
|
706
725
|
# Re-export types needed by TYPE_CHECKING imports
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mixinv2-0.3.0.post57.dev0 → mixinv2-0.3.0.post58.dev0}/src/mixinv2/_interned_linked_list.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|