forwardpy 0.2.0__tar.gz → 0.3.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.
- {forwardpy-0.2.0 → forwardpy-0.3.0}/LICENSE +21 -21
- {forwardpy-0.2.0/src/forwardpy.egg-info → forwardpy-0.3.0}/PKG-INFO +1 -1
- {forwardpy-0.2.0 → forwardpy-0.3.0}/pyproject.toml +1 -1
- forwardpy-0.3.0/src/forwardpy/__init__.py +10 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/src/forwardpy/core.py +57 -1
- {forwardpy-0.2.0 → forwardpy-0.3.0/src/forwardpy.egg-info}/PKG-INFO +1 -1
- {forwardpy-0.2.0 → forwardpy-0.3.0}/src/forwardpy.egg-info/SOURCES.txt +2 -1
- forwardpy-0.3.0/tests/test_unregister.py +196 -0
- forwardpy-0.2.0/src/forwardpy/__init__.py +0 -10
- {forwardpy-0.2.0 → forwardpy-0.3.0}/README.md +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/setup.cfg +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/src/forwardpy/py.typed +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/src/forwardpy.egg-info/dependency_links.txt +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/src/forwardpy.egg-info/requires.txt +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/src/forwardpy.egg-info/top_level.txt +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/tests/test_basic.py +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/tests/test_classmethod_staticmethod.py +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/tests/test_extension.py +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/tests/test_impl.py +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/tests/test_inheritance.py +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/tests/test_object.py +0 -0
- {forwardpy-0.2.0 → forwardpy-0.3.0}/tests/test_property.py +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 forwardpy contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 forwardpy contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -12,7 +12,7 @@ import textwrap
|
|
|
12
12
|
import weakref
|
|
13
13
|
from typing import TypeVar, Generic, Callable, Any, get_type_hints, TYPE_CHECKING
|
|
14
14
|
|
|
15
|
-
__all__ = ["Object", "Extension", "impl"]
|
|
15
|
+
__all__ = ["Object", "Extension", "impl", "unregister_module_impls"]
|
|
16
16
|
|
|
17
17
|
# 类型变量
|
|
18
18
|
T = TypeVar("T", bound="Object")
|
|
@@ -26,6 +26,10 @@ _attribute_registry: dict[type, dict[str, Any]] = {}
|
|
|
26
26
|
# 全局 property 注册表: {类: {property名: ForwardpyProperty}}
|
|
27
27
|
_property_registry: dict[type, dict[str, "ForwardpyProperty"]] = {}
|
|
28
28
|
|
|
29
|
+
# impl 来源追踪: {(类, impl_key) -> 来源模块名}
|
|
30
|
+
# impl_key 为 method_name、"prop_name.getter" 或 "prop_name.setter"
|
|
31
|
+
_impl_sources: dict[tuple[type, str], str] = {}
|
|
32
|
+
|
|
29
33
|
# 声明方法标记(用于标识未实现的方法)
|
|
30
34
|
_DECLARED_METHODS: str = "__forwardpy_declared_methods__"
|
|
31
35
|
|
|
@@ -460,6 +464,7 @@ def impl(
|
|
|
460
464
|
f"Use @forwardpy.impl({prop.owner_cls.__name__}.{prop.name}.getter, override=True) to override."
|
|
461
465
|
)
|
|
462
466
|
prop._fget = func
|
|
467
|
+
_impl_sources[(prop.owner_cls, f"{prop.name}.getter")] = getattr(func, "__module__", "") or ""
|
|
463
468
|
return func
|
|
464
469
|
|
|
465
470
|
# 处理 property setter
|
|
@@ -471,6 +476,7 @@ def impl(
|
|
|
471
476
|
f"Use @forwardpy.impl({prop.owner_cls.__name__}.{prop.name}.setter, override=True) to override."
|
|
472
477
|
)
|
|
473
478
|
prop._fset = func
|
|
479
|
+
_impl_sources[(prop.owner_cls, f"{prop.name}.setter")] = getattr(func, "__module__", "") or ""
|
|
474
480
|
return func
|
|
475
481
|
|
|
476
482
|
# 获取目标类和方法名
|
|
@@ -534,6 +540,9 @@ def impl(
|
|
|
534
540
|
_method_registry[target_cls] = {}
|
|
535
541
|
_method_registry[target_cls][method_name] = func
|
|
536
542
|
|
|
543
|
+
# 记录来源模块
|
|
544
|
+
_impl_sources[(target_cls, method_name)] = getattr(func, "__module__", "") or ""
|
|
545
|
+
|
|
537
546
|
# 保留类引用,便于后续 override
|
|
538
547
|
func.__forwardpy_class__ = target_cls
|
|
539
548
|
func.__name__ = method_name
|
|
@@ -550,3 +559,50 @@ def impl(
|
|
|
550
559
|
return func
|
|
551
560
|
|
|
552
561
|
return decorator
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def unregister_module_impls(module_name: str) -> int:
|
|
565
|
+
"""移除指定模块注册的所有 @impl,恢复为 stub
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
module_name: 来源模块的 __name__
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
被卸载的 impl 数量
|
|
572
|
+
"""
|
|
573
|
+
to_remove: list[tuple[type, str]] = [
|
|
574
|
+
(cls, impl_key)
|
|
575
|
+
for (cls, impl_key), src in _impl_sources.items()
|
|
576
|
+
if src == module_name
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
for cls, impl_key in to_remove:
|
|
580
|
+
del _impl_sources[(cls, impl_key)]
|
|
581
|
+
|
|
582
|
+
if impl_key.endswith(".getter"):
|
|
583
|
+
prop_name = impl_key[:-7]
|
|
584
|
+
prop = _property_registry.get(cls, {}).get(prop_name)
|
|
585
|
+
if prop is not None:
|
|
586
|
+
prop._fget = None
|
|
587
|
+
elif impl_key.endswith(".setter"):
|
|
588
|
+
prop_name = impl_key[:-7]
|
|
589
|
+
prop = _property_registry.get(cls, {}).get(prop_name)
|
|
590
|
+
if prop is not None:
|
|
591
|
+
prop._fset = None
|
|
592
|
+
else:
|
|
593
|
+
# 普通方法、classmethod 或 staticmethod
|
|
594
|
+
method_name = impl_key
|
|
595
|
+
if cls in _method_registry and method_name in _method_registry[cls]:
|
|
596
|
+
del _method_registry[cls][method_name]
|
|
597
|
+
|
|
598
|
+
declared_classmethods = getattr(cls, _DECLARED_CLASSMETHODS, set())
|
|
599
|
+
declared_staticmethods = getattr(cls, _DECLARED_STATICMETHODS, set())
|
|
600
|
+
|
|
601
|
+
if method_name in declared_classmethods:
|
|
602
|
+
setattr(cls, method_name, _make_stub_classmethod(method_name, cls))
|
|
603
|
+
elif method_name in declared_staticmethods:
|
|
604
|
+
setattr(cls, method_name, _make_stub_staticmethod(method_name, cls))
|
|
605
|
+
else:
|
|
606
|
+
setattr(cls, method_name, _make_stub_method(method_name, cls))
|
|
607
|
+
|
|
608
|
+
return len(to_remove)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Tests for impl source tracking and unregister_module_impls."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import forwardpy
|
|
5
|
+
from forwardpy.core import (
|
|
6
|
+
_impl_sources,
|
|
7
|
+
_method_registry,
|
|
8
|
+
_property_registry,
|
|
9
|
+
_DECLARED_METHODS,
|
|
10
|
+
unregister_module_impls,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestImplSourceTracking:
|
|
15
|
+
|
|
16
|
+
def test_impl_records_source_module(self):
|
|
17
|
+
class Svc(forwardpy.Object):
|
|
18
|
+
def run(self) -> str: ...
|
|
19
|
+
|
|
20
|
+
@forwardpy.impl(Svc.run)
|
|
21
|
+
def run(self: Svc) -> str:
|
|
22
|
+
return "ok"
|
|
23
|
+
|
|
24
|
+
assert (Svc, "run") in _impl_sources
|
|
25
|
+
assert _impl_sources[(Svc, "run")] == __name__
|
|
26
|
+
|
|
27
|
+
def test_impl_override_updates_source(self):
|
|
28
|
+
class Svc2(forwardpy.Object):
|
|
29
|
+
def run(self) -> str: ...
|
|
30
|
+
|
|
31
|
+
@forwardpy.impl(Svc2.run)
|
|
32
|
+
def run_v1(self) -> str:
|
|
33
|
+
return "v1"
|
|
34
|
+
|
|
35
|
+
old_source = _impl_sources[(Svc2, "run")]
|
|
36
|
+
|
|
37
|
+
@forwardpy.impl(Svc2.run, override=True)
|
|
38
|
+
def run_v2(self) -> str:
|
|
39
|
+
return "v2"
|
|
40
|
+
|
|
41
|
+
assert _impl_sources[(Svc2, "run")] == old_source # same test module
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestUnregisterModuleImpls:
|
|
45
|
+
|
|
46
|
+
def test_unregister_restores_stub_method(self):
|
|
47
|
+
class A(forwardpy.Object):
|
|
48
|
+
def do_it(self) -> str: ...
|
|
49
|
+
|
|
50
|
+
@forwardpy.impl(A.do_it)
|
|
51
|
+
def do_it(self) -> str:
|
|
52
|
+
return "done"
|
|
53
|
+
|
|
54
|
+
a = A()
|
|
55
|
+
assert a.do_it() == "done"
|
|
56
|
+
|
|
57
|
+
source_module = _impl_sources[(A, "do_it")]
|
|
58
|
+
count = unregister_module_impls(source_module)
|
|
59
|
+
assert count >= 1
|
|
60
|
+
|
|
61
|
+
with pytest.raises(NotImplementedError):
|
|
62
|
+
a.do_it()
|
|
63
|
+
|
|
64
|
+
def test_unregister_restores_stub_classmethod(self):
|
|
65
|
+
class B(forwardpy.Object):
|
|
66
|
+
@classmethod
|
|
67
|
+
def create(cls) -> str: ...
|
|
68
|
+
|
|
69
|
+
@forwardpy.impl(B.create)
|
|
70
|
+
def create(cls) -> str:
|
|
71
|
+
return "created"
|
|
72
|
+
|
|
73
|
+
assert B.create() == "created"
|
|
74
|
+
|
|
75
|
+
source_module = _impl_sources[(B, "create")]
|
|
76
|
+
unregister_module_impls(source_module)
|
|
77
|
+
|
|
78
|
+
with pytest.raises(NotImplementedError):
|
|
79
|
+
B.create()
|
|
80
|
+
|
|
81
|
+
def test_unregister_restores_stub_staticmethod(self):
|
|
82
|
+
class C(forwardpy.Object):
|
|
83
|
+
@staticmethod
|
|
84
|
+
def helper() -> str: ...
|
|
85
|
+
|
|
86
|
+
@forwardpy.impl(C.helper)
|
|
87
|
+
def helper() -> str:
|
|
88
|
+
return "helped"
|
|
89
|
+
|
|
90
|
+
assert C.helper() == "helped"
|
|
91
|
+
|
|
92
|
+
source_module = _impl_sources[(C, "helper")]
|
|
93
|
+
unregister_module_impls(source_module)
|
|
94
|
+
|
|
95
|
+
with pytest.raises(NotImplementedError):
|
|
96
|
+
C.helper()
|
|
97
|
+
|
|
98
|
+
def test_unregister_restores_property_getter(self):
|
|
99
|
+
class D(forwardpy.Object):
|
|
100
|
+
@property
|
|
101
|
+
def value(self) -> int: ...
|
|
102
|
+
|
|
103
|
+
@forwardpy.impl(D.value.getter)
|
|
104
|
+
def get_value(self) -> int:
|
|
105
|
+
return 42
|
|
106
|
+
|
|
107
|
+
d = D()
|
|
108
|
+
assert d.value == 42
|
|
109
|
+
|
|
110
|
+
source_module = _impl_sources[(D, "value.getter")]
|
|
111
|
+
unregister_module_impls(source_module)
|
|
112
|
+
|
|
113
|
+
with pytest.raises(NotImplementedError):
|
|
114
|
+
_ = d.value
|
|
115
|
+
|
|
116
|
+
def test_unregister_returns_count(self):
|
|
117
|
+
class E(forwardpy.Object):
|
|
118
|
+
def m1(self) -> str: ...
|
|
119
|
+
def m2(self) -> str: ...
|
|
120
|
+
|
|
121
|
+
# Use a unique fake module name to isolate
|
|
122
|
+
@forwardpy.impl(E.m1)
|
|
123
|
+
def m1(self) -> str:
|
|
124
|
+
return "1"
|
|
125
|
+
|
|
126
|
+
@forwardpy.impl(E.m2)
|
|
127
|
+
def m2(self) -> str:
|
|
128
|
+
return "2"
|
|
129
|
+
|
|
130
|
+
source_module = _impl_sources[(E, "m1")]
|
|
131
|
+
count = unregister_module_impls(source_module)
|
|
132
|
+
assert count >= 2
|
|
133
|
+
|
|
134
|
+
def test_unregister_only_removes_matching_module(self):
|
|
135
|
+
class F(forwardpy.Object):
|
|
136
|
+
def alpha(self) -> str: ...
|
|
137
|
+
def beta(self) -> str: ...
|
|
138
|
+
|
|
139
|
+
# Register alpha from "module_a"
|
|
140
|
+
def alpha_impl(self) -> str:
|
|
141
|
+
return "a"
|
|
142
|
+
alpha_impl.__module__ = "module_a"
|
|
143
|
+
forwardpy.impl(F.alpha)(alpha_impl)
|
|
144
|
+
|
|
145
|
+
# Register beta from "module_b"
|
|
146
|
+
def beta_impl(self) -> str:
|
|
147
|
+
return "b"
|
|
148
|
+
beta_impl.__module__ = "module_b"
|
|
149
|
+
forwardpy.impl(F.beta)(beta_impl)
|
|
150
|
+
|
|
151
|
+
f = F()
|
|
152
|
+
assert f.alpha() == "a"
|
|
153
|
+
assert f.beta() == "b"
|
|
154
|
+
|
|
155
|
+
# Unregister only module_a
|
|
156
|
+
count = unregister_module_impls("module_a")
|
|
157
|
+
assert count == 1
|
|
158
|
+
|
|
159
|
+
with pytest.raises(NotImplementedError):
|
|
160
|
+
f.alpha()
|
|
161
|
+
# beta should still work
|
|
162
|
+
assert f.beta() == "b"
|
|
163
|
+
|
|
164
|
+
def test_unregister_removes_from_method_registry(self):
|
|
165
|
+
class G(forwardpy.Object):
|
|
166
|
+
def work(self) -> str: ...
|
|
167
|
+
|
|
168
|
+
@forwardpy.impl(G.work)
|
|
169
|
+
def work(self) -> str:
|
|
170
|
+
return "working"
|
|
171
|
+
|
|
172
|
+
assert "work" in _method_registry.get(G, {})
|
|
173
|
+
|
|
174
|
+
source_module = _impl_sources[(G, "work")]
|
|
175
|
+
unregister_module_impls(source_module)
|
|
176
|
+
|
|
177
|
+
assert "work" not in _method_registry.get(G, {})
|
|
178
|
+
|
|
179
|
+
def test_unregister_nonexistent_module_is_noop(self):
|
|
180
|
+
count = unregister_module_impls("nonexistent.module.xyz")
|
|
181
|
+
assert count == 0
|
|
182
|
+
|
|
183
|
+
def test_unregister_removes_from_impl_sources(self):
|
|
184
|
+
class H(forwardpy.Object):
|
|
185
|
+
def proc(self) -> str: ...
|
|
186
|
+
|
|
187
|
+
@forwardpy.impl(H.proc)
|
|
188
|
+
def proc(self) -> str:
|
|
189
|
+
return "processed"
|
|
190
|
+
|
|
191
|
+
assert (H, "proc") in _impl_sources
|
|
192
|
+
|
|
193
|
+
source_module = _impl_sources[(H, "proc")]
|
|
194
|
+
unregister_module_impls(source_module)
|
|
195
|
+
|
|
196
|
+
assert (H, "proc") not in _impl_sources
|
|
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
|