lalamo 0.3.1__tar.gz → 0.3.2__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 (69) hide show
  1. {lalamo-0.3.1 → lalamo-0.3.2}/PKG-INFO +3 -2
  2. {lalamo-0.3.1 → lalamo-0.3.2}/README.md +2 -1
  3. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/__init__.py +1 -1
  4. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/common.py +2 -1
  5. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/common.py +42 -0
  6. lalamo-0.3.2/lalamo/registry_abc.py +63 -0
  7. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo.egg-info/PKG-INFO +3 -2
  8. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo.egg-info/SOURCES.txt +4 -1
  9. lalamo-0.3.2/tests/test_model_spec.py +52 -0
  10. lalamo-0.3.2/tests/test_registry_abc.py +148 -0
  11. {lalamo-0.3.1 → lalamo-0.3.2}/LICENSE +0 -0
  12. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/common.py +0 -0
  13. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/language_model.py +0 -0
  14. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/main.py +0 -0
  15. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/message_processor.py +0 -0
  16. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/__init__.py +0 -0
  17. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/common.py +0 -0
  18. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/__init__.py +0 -0
  19. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/executorch.py +0 -0
  20. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/huggingface/__init__.py +0 -0
  21. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/huggingface/common.py +0 -0
  22. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/huggingface/gemma2.py +0 -0
  23. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/huggingface/gemma3.py +0 -0
  24. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/huggingface/llama.py +0 -0
  25. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/huggingface/mistral.py +0 -0
  26. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/huggingface/qwen2.py +0 -0
  27. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/decoder_configs/huggingface/qwen3.py +0 -0
  28. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/huggingface_generation_config.py +0 -0
  29. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/huggingface_tokenizer_config.py +0 -0
  30. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/loaders/__init__.py +0 -0
  31. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/loaders/common.py +0 -0
  32. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/loaders/executorch.py +0 -0
  33. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/loaders/huggingface.py +0 -0
  34. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/__init__.py +0 -0
  35. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/deepseek.py +0 -0
  36. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/gemma.py +0 -0
  37. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/huggingface.py +0 -0
  38. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/llama.py +0 -0
  39. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/mistral.py +0 -0
  40. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/pleias.py +0 -0
  41. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/polaris.py +0 -0
  42. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/qwen.py +0 -0
  43. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/model_import/model_specs/reka.py +0 -0
  44. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/__init__.py +0 -0
  45. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/activations.py +0 -0
  46. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/attention.py +0 -0
  47. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/common.py +0 -0
  48. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/decoder.py +0 -0
  49. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/decoder_layer.py +0 -0
  50. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/embedding.py +0 -0
  51. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/kv_cache.py +0 -0
  52. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/linear.py +0 -0
  53. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/mlp.py +0 -0
  54. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/normalization.py +0 -0
  55. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/rope.py +0 -0
  56. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/torch_interop.py +0 -0
  57. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/modules/utils.py +0 -0
  58. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/quantization.py +0 -0
  59. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/sampling.py +0 -0
  60. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo/utils.py +0 -0
  61. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo.egg-info/dependency_links.txt +0 -0
  62. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo.egg-info/entry_points.txt +0 -0
  63. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo.egg-info/requires.txt +0 -0
  64. {lalamo-0.3.1 → lalamo-0.3.2}/lalamo.egg-info/top_level.txt +0 -0
  65. {lalamo-0.3.1 → lalamo-0.3.2}/pyproject.toml +0 -0
  66. {lalamo-0.3.1 → lalamo-0.3.2}/setup.cfg +0 -0
  67. {lalamo-0.3.1 → lalamo-0.3.2}/tests/test_generation.py +0 -0
  68. {lalamo-0.3.1 → lalamo-0.3.2}/tests/test_huggingface_models.py +0 -0
  69. {lalamo-0.3.1 → lalamo-0.3.2}/tests/test_parameter_tree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lalamo
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: JAX library for optimization and export of models for use with the UZU inference engine.
5
5
  Requires-Python: <4,>=3.12
6
6
  Description-Content-Type: text/markdown
@@ -32,7 +32,7 @@ Dynamic: license-file
32
32
  <a href="https://artifacts.trymirai.com/social/about_us.mp3"><img src="https://img.shields.io/badge/Listen-Podcast-red" alt="Listen to our podcast"></a>
33
33
  <a href="https://docsend.com/v/76bpr/mirai2025"><img src="https://img.shields.io/badge/View-Deck-red" alt="View our deck"></a>
34
34
  <a href="mailto:alexey@getmirai.co,dima@getmirai.co,aleksei@getmirai.co?subject=Interested%20in%20Mirai"><img src="https://img.shields.io/badge/Send-Email-green" alt="Contact us"></a>
35
- <a href="https://docs.trymirai.com/components/models"><img src="https://img.shields.io/badge/Read-Docs-blue" alt="Read docs"></a>
35
+ <a href="https://docs.trymirai.com/overview/lalamo"><img src="https://img.shields.io/badge/Read-Docs-blue" alt="Read docs"></a>
36
36
  [![License](https://img.shields.io/badge/License-MIT-blue)](LICENSE)
37
37
 
38
38
  # lalamo
@@ -73,3 +73,4 @@ ModelSpec(
73
73
  weights_type=WeightsType.SAFETENSORS,
74
74
  )
75
75
  ```
76
+
@@ -7,7 +7,7 @@
7
7
  <a href="https://artifacts.trymirai.com/social/about_us.mp3"><img src="https://img.shields.io/badge/Listen-Podcast-red" alt="Listen to our podcast"></a>
8
8
  <a href="https://docsend.com/v/76bpr/mirai2025"><img src="https://img.shields.io/badge/View-Deck-red" alt="View our deck"></a>
9
9
  <a href="mailto:alexey@getmirai.co,dima@getmirai.co,aleksei@getmirai.co?subject=Interested%20in%20Mirai"><img src="https://img.shields.io/badge/Send-Email-green" alt="Contact us"></a>
10
- <a href="https://docs.trymirai.com/components/models"><img src="https://img.shields.io/badge/Read-Docs-blue" alt="Read docs"></a>
10
+ <a href="https://docs.trymirai.com/overview/lalamo"><img src="https://img.shields.io/badge/Read-Docs-blue" alt="Read docs"></a>
11
11
  [![License](https://img.shields.io/badge/License-MIT-blue)](LICENSE)
12
12
 
13
13
  # lalamo
@@ -48,3 +48,4 @@ ModelSpec(
48
48
  weights_type=WeightsType.SAFETENSORS,
49
49
  )
50
50
  ```
51
+
@@ -1,7 +1,7 @@
1
1
  from lalamo.model_import import REPO_TO_MODEL, ModelSpec, import_model
2
2
  from lalamo.modules import Decoder
3
3
 
4
- __version__ = "0.3.1"
4
+ __version__ = "0.3.2"
5
5
 
6
6
  __all__ = [
7
7
  "REPO_TO_MODEL",
@@ -9,12 +9,13 @@ import cattrs
9
9
  from jaxtyping import Array, DTypeLike
10
10
 
11
11
  from lalamo.modules import Decoder, DecoderConfig
12
+ from lalamo.registry_abc import RegistryABC
12
13
 
13
14
  __all__ = ["ForeignConfig"]
14
15
 
15
16
 
16
17
  @dataclass(frozen=True)
17
- class ForeignConfig:
18
+ class ForeignConfig(RegistryABC):
18
19
  _converter: ClassVar[cattrs.Converter] = cattrs.Converter()
19
20
  _converter.register_structure_hook(int | list[int], lambda v, _: v)
20
21
 
@@ -1,10 +1,13 @@
1
1
  from collections.abc import (
2
+ Callable,
2
3
  Mapping,
3
4
  )
4
5
  from dataclasses import dataclass, field
5
6
  from enum import Enum
6
7
  from pathlib import Path
8
+ from typing import ClassVar, cast, get_args, get_origin
7
9
 
10
+ import cattrs
8
11
  import jax.numpy as jnp
9
12
  from jaxtyping import Array, DTypeLike
10
13
  from safetensors.flax import load_file as load_safetensors
@@ -65,8 +68,40 @@ class ConfigMap:
65
68
  chat_template: FileSpec | None = None
66
69
 
67
70
 
71
+ def _is_foreign_config_type(t: object) -> bool:
72
+ origin = get_origin(t)
73
+ args = get_args(t)
74
+ return origin is type and len(args) == 1 and isinstance(args[0], type) and issubclass(args[0], ForeignConfig)
75
+
76
+
77
+ def _structure_foreign_config_factory(
78
+ t: object,
79
+ c: cattrs.Converter,
80
+ ) -> Callable[[object, object], type[ForeignConfig]]:
81
+ name_to_type = {t.__name__: t for t in ForeignConfig.__descendants__()}
82
+
83
+ def _hook(v: object, _t: object) -> type[ForeignConfig]:
84
+ if isinstance(v, type) and issubclass(v, ForeignConfig):
85
+ return v
86
+ return name_to_type[cast("str", v)]
87
+
88
+ return _hook
89
+
90
+
91
+ def _unstructure_foreign_config_factory(t: object, c: cattrs.Converter) -> Callable[[type[ForeignConfig]], str]:
92
+ def _hook(v: type[ForeignConfig]) -> str:
93
+ return v.__name__
94
+
95
+ return _hook
96
+
97
+
68
98
  @dataclass(frozen=True)
69
99
  class ModelSpec:
100
+ _converter: ClassVar[cattrs.Converter] = cattrs.Converter()
101
+
102
+ _converter.register_structure_hook_factory(_is_foreign_config_type, _structure_foreign_config_factory)
103
+ _converter.register_unstructure_hook_factory(_is_foreign_config_type, _unstructure_foreign_config_factory)
104
+
70
105
  vendor: str
71
106
  family: str
72
107
  name: str
@@ -83,6 +118,13 @@ class ModelSpec:
83
118
  configs: ConfigMap = field(default=ConfigMap())
84
119
  use_cases: tuple[UseCase, ...] = tuple()
85
120
 
121
+ @classmethod
122
+ def from_json(cls, json_data: dict) -> "ModelSpec":
123
+ return cls._converter.structure(json_data, cls)
124
+
125
+ def to_json(self) -> dict:
126
+ return self._converter.unstructure(self)
127
+
86
128
 
87
129
  def awq_model_spec(
88
130
  model_spec: ModelSpec,
@@ -0,0 +1,63 @@
1
+ from abc import ABC, ABCMeta
2
+ from typing import Any
3
+ from weakref import WeakSet
4
+
5
+ __all__ = ["RegistryABC", "RegistryMeta"]
6
+
7
+
8
+ class RegistryMeta(ABCMeta):
9
+ """
10
+ Metaclass that tracks, for each subclass of RegistryABC, a per-class WeakSet
11
+ of descendants (classes that have it in their MRO) while excluding any class
12
+ that directly lists `RegistryABC` among its bases.
13
+ """
14
+
15
+ _REG_ATTR: str = "__registry_descendants__"
16
+ _ROOT: type["RegistryABC"] | None = None
17
+
18
+ def __init__(
19
+ cls: type,
20
+ name: str,
21
+ bases: tuple[type, ...],
22
+ namespace: dict[str, object],
23
+ **kwargs: Any, # noqa: ANN401
24
+ ) -> None:
25
+ super().__init__(name, bases, namespace, **kwargs) # type: ignore[call-overload]
26
+
27
+ # Give *every* class its own WeakSet (shadow any inherited attribute)
28
+ setattr(cls, RegistryMeta._REG_ATTR, WeakSet())
29
+
30
+ # Detect and remember the root exactly once
31
+ if RegistryMeta._ROOT is None and name == "RegistryABC":
32
+ RegistryMeta._ROOT = cls # type: ignore[assignment]
33
+ return
34
+
35
+ root = RegistryMeta._ROOT
36
+ if root is None:
37
+ # Extremely early import edge-case; nothing to register yet
38
+ return
39
+
40
+ # Exclude classes that directly list the root among bases
41
+ if any(b is root for b in cls.__bases__):
42
+ return
43
+
44
+ # Register this class on all qualifying ancestors below the root
45
+ for ancestor in cls.__mro__[1:]:
46
+ if isinstance(ancestor, RegistryMeta) and issubclass(ancestor, root):
47
+ getattr(ancestor, RegistryMeta._REG_ATTR).add(cls)
48
+
49
+
50
+ class RegistryABC(ABC, metaclass=RegistryMeta):
51
+ """
52
+ Abstract base tracked by RegistryMeta.
53
+
54
+ Any class defined as `class AbstractFoo(RegistryABC)` will expose a
55
+ class method `AbstractFoo.__get_descendants__()` that returns a list of
56
+ all classes having AbstractFoo in their MRO *except* those that directly
57
+ include `RegistryABC` among their bases.
58
+ """
59
+
60
+ @classmethod
61
+ def __descendants__(cls) -> tuple[type, ...]:
62
+ reg: WeakSet[type] = getattr(cls, RegistryMeta._REG_ATTR) # noqa: SLF001
63
+ return tuple(reg)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lalamo
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: JAX library for optimization and export of models for use with the UZU inference engine.
5
5
  Requires-Python: <4,>=3.12
6
6
  Description-Content-Type: text/markdown
@@ -32,7 +32,7 @@ Dynamic: license-file
32
32
  <a href="https://artifacts.trymirai.com/social/about_us.mp3"><img src="https://img.shields.io/badge/Listen-Podcast-red" alt="Listen to our podcast"></a>
33
33
  <a href="https://docsend.com/v/76bpr/mirai2025"><img src="https://img.shields.io/badge/View-Deck-red" alt="View our deck"></a>
34
34
  <a href="mailto:alexey@getmirai.co,dima@getmirai.co,aleksei@getmirai.co?subject=Interested%20in%20Mirai"><img src="https://img.shields.io/badge/Send-Email-green" alt="Contact us"></a>
35
- <a href="https://docs.trymirai.com/components/models"><img src="https://img.shields.io/badge/Read-Docs-blue" alt="Read docs"></a>
35
+ <a href="https://docs.trymirai.com/overview/lalamo"><img src="https://img.shields.io/badge/Read-Docs-blue" alt="Read docs"></a>
36
36
  [![License](https://img.shields.io/badge/License-MIT-blue)](LICENSE)
37
37
 
38
38
  # lalamo
@@ -73,3 +73,4 @@ ModelSpec(
73
73
  weights_type=WeightsType.SAFETENSORS,
74
74
  )
75
75
  ```
76
+
@@ -7,6 +7,7 @@ lalamo/language_model.py
7
7
  lalamo/main.py
8
8
  lalamo/message_processor.py
9
9
  lalamo/quantization.py
10
+ lalamo/registry_abc.py
10
11
  lalamo/sampling.py
11
12
  lalamo/utils.py
12
13
  lalamo.egg-info/PKG-INFO
@@ -61,4 +62,6 @@ lalamo/modules/torch_interop.py
61
62
  lalamo/modules/utils.py
62
63
  tests/test_generation.py
63
64
  tests/test_huggingface_models.py
64
- tests/test_parameter_tree.py
65
+ tests/test_model_spec.py
66
+ tests/test_parameter_tree.py
67
+ tests/test_registry_abc.py
@@ -0,0 +1,52 @@
1
+ from lalamo.model_import.decoder_configs.huggingface.llama import HFLlamaConfig
2
+ from lalamo.model_import.model_specs.common import ModelSpec, WeightsType
3
+
4
+ EXAMPLE_JSON = {
5
+ "vendor": "Meta",
6
+ "family": "Llama-3.2",
7
+ "name": "Llama-3.2-1B-Instruct",
8
+ "size": "1B",
9
+ "quantization": None,
10
+ "repo": "meta-llama/Llama-3.2-1B-Instruct",
11
+ "config_type": "HFLlamaConfig",
12
+ "output_parser_regex": None,
13
+ "system_role_name": "system",
14
+ "user_role_name": "user",
15
+ "assistant_role_name": "assistant",
16
+ "tool_role_name": "tool",
17
+ "weights_type": "safetensors",
18
+ "use_cases": [],
19
+ }
20
+
21
+
22
+ def test_deserialization() -> None:
23
+ model_spec = ModelSpec.from_json(EXAMPLE_JSON)
24
+ assert model_spec.vendor == "Meta"
25
+ assert model_spec.family == "Llama-3.2"
26
+ assert model_spec.name == "Llama-3.2-1B-Instruct"
27
+ assert model_spec.size == "1B"
28
+ assert model_spec.quantization is None
29
+ assert model_spec.repo == "meta-llama/Llama-3.2-1B-Instruct"
30
+ assert model_spec.config_type is HFLlamaConfig
31
+ assert model_spec.output_parser_regex is None
32
+ assert model_spec.system_role_name == "system"
33
+ assert model_spec.user_role_name == "user"
34
+ assert model_spec.assistant_role_name == "assistant"
35
+ assert model_spec.tool_role_name == "tool"
36
+ assert model_spec.weights_type == WeightsType.SAFETENSORS
37
+ assert model_spec.use_cases == ()
38
+
39
+
40
+ def test_consistency() -> None:
41
+ spec = ModelSpec(
42
+ vendor="Meta",
43
+ family="Llama-3.2",
44
+ name="Llama-3.2-1B-Instruct",
45
+ size="1B",
46
+ quantization=None,
47
+ repo="meta-llama/Llama-3.2-1B-Instruct",
48
+ config_type=HFLlamaConfig,
49
+ use_cases=tuple(),
50
+ )
51
+
52
+ assert ModelSpec.from_json(spec.to_json()) == spec
@@ -0,0 +1,148 @@
1
+ # test_registry_abc.py
2
+ import gc
3
+ import weakref
4
+
5
+ from lalamo.registry_abc import RegistryABC
6
+
7
+
8
+ def test_basic_registration_and_multilevel() -> None:
9
+ class AbstractFoo(RegistryABC):
10
+ pass
11
+
12
+ class A(AbstractFoo):
13
+ pass
14
+
15
+ class B(A):
16
+ pass
17
+
18
+ # __get_descendants__ is a class method returning a list
19
+ assert isinstance(AbstractFoo.__descendants__(), list)
20
+
21
+ # Order is not guaranteed; compare as sets
22
+ assert set(AbstractFoo.__descendants__()) >= {A, B}
23
+ assert set(A.__descendants__()) == {B}
24
+ assert set(B.__descendants__()) == set()
25
+
26
+
27
+ def test_excludes_classes_that_directly_list_registryabc() -> None:
28
+ class AbstractFoo(RegistryABC):
29
+ pass
30
+
31
+ class A(AbstractFoo):
32
+ pass
33
+
34
+ # This class directly lists RegistryABC, so it must be excluded everywhere.
35
+ class Hybrid(AbstractFoo, RegistryABC):
36
+ pass
37
+
38
+ assert A in AbstractFoo.__descendants__() # tracked
39
+ assert Hybrid not in AbstractFoo.__descendants__() # excluded
40
+
41
+
42
+ def test_no_cross_pollination_between_roots() -> None:
43
+ class AbstractFoo(RegistryABC):
44
+ pass
45
+
46
+ class AbstractBar(RegistryABC):
47
+ pass
48
+
49
+ class AFoo(AbstractFoo):
50
+ pass
51
+
52
+ class BBar(AbstractBar):
53
+ pass
54
+
55
+ assert AFoo in AbstractFoo.__descendants__() # in its own tree
56
+ assert BBar not in AbstractFoo.__descendants__() # not leaking across
57
+
58
+ assert BBar in AbstractBar.__descendants__() # in its own tree
59
+ assert AFoo not in AbstractBar.__descendants__() # not leaking across
60
+
61
+
62
+ def test_mixin_and_indirect_registryabc_paths_are_included() -> None:
63
+ class AbstractFoo(RegistryABC):
64
+ pass
65
+
66
+ class OtherRoot(RegistryABC):
67
+ pass
68
+
69
+ class Mixin:
70
+ pass
71
+
72
+ # Indirect path via a base that itself descends from RegistryABC should *not*
73
+ # exclude the class, because the class does not directly list RegistryABC.
74
+ class Indirect(OtherRoot):
75
+ pass
76
+
77
+ class D(Mixin, AbstractFoo, Indirect):
78
+ pass
79
+
80
+ assert D in AbstractFoo.__descendants__()
81
+
82
+
83
+ def test_dynamic_updates_on_late_definitions() -> None:
84
+ class AbstractFoo(RegistryABC):
85
+ pass
86
+
87
+ assert set(AbstractFoo.__descendants__()) == set()
88
+
89
+ class A(AbstractFoo):
90
+ pass
91
+
92
+ assert A in AbstractFoo.__descendants__()
93
+
94
+ class B(A):
95
+ pass
96
+
97
+ # Newly defined class appears without needing to "refresh" anything
98
+ assert B in AbstractFoo.__descendants__()
99
+ # And it should also appear under A
100
+ assert B in A.__descendants__()
101
+
102
+
103
+ def test_weakset_allows_garbage_collection() -> None:
104
+ class AbstractFoo(RegistryABC):
105
+ pass
106
+
107
+ def make_temp() -> type:
108
+ class Temp(AbstractFoo):
109
+ pass
110
+
111
+ return Temp
112
+
113
+ temp_cls = make_temp()
114
+ wr = weakref.ref(temp_cls)
115
+
116
+ # It should be registered while alive
117
+ assert temp_cls in AbstractFoo.__descendants__()
118
+ assert wr() is not None
119
+
120
+ # Drop the strong ref and collect
121
+ del temp_cls
122
+ gc.collect()
123
+
124
+ # Weak ref is cleared, and the registry reflects that
125
+ assert wr() is None
126
+ names = {cls.__name__ for cls in AbstractFoo.__descendants__()}
127
+ assert "Temp" not in names
128
+
129
+
130
+ def test_descendants_method_exists_on_all_registryabc_subclasses() -> None:
131
+ class Root(RegistryABC):
132
+ pass
133
+
134
+ class Mid(Root):
135
+ pass
136
+
137
+ class Leaf(Mid):
138
+ pass
139
+
140
+ # Method exists and returns lists on all levels
141
+ for C in (Root, Mid, Leaf): # noqa: N806
142
+ assert hasattr(C, "__get_descendants__")
143
+ assert callable(C.__descendants__)
144
+ assert isinstance(C.__descendants__(), list)
145
+
146
+ assert Leaf in Mid.__descendants__()
147
+ assert Leaf in Root.__descendants__()
148
+ assert Mid in Root.__descendants__()
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
File without changes
File without changes
File without changes