Skip to content

统一 create_module 签名;停止把 Protocol 当 mixin 基类;清理 links.py 的重复分派与 Mock 兜底 #161

Description

@SongshGeo

三个同源问题:内部契约(签名、继承、分派)在不同实现之间各写各的。


1. create_module 有四套互不兼容的签名

位置 签名 返回
core/protocols.py (name: str, *args, **kwargs) ModuleProtocol
core/base_subsystem.py@abstractmethod (module_cls: Type[BaseModule], *args, **kwargs) Any
space/nature.py (*args, module_cls=PatchModule, major_layer=False, write_crs=False, **kwargs) PatchModule
human/human.py (name=None, *, module_cls=HumanModule, **kwargs) BaseModule

同一句 subsystem.create_module(SomeCls)

  • BaseHuman 上,SomeCls 绑到 name
  • BaseNature 上,落进 *args

行为完全不同,而且协议里声明的那一套没有任何实现匹配。这是"看起来有协议约束、实际没有"的典型。

建议:定一个签名(推荐 create_module(module_cls, *, name=None, **kwargs)),协议、抽象方法、两个实现全部对齐;子类特有的参数(major_layer / write_crs)保留为关键字。旧调用形式按弃用流程处理。


2. Protocol 被当成真实基类继承

core/protocols.py 里的 Protocol 出现在 5 处 MRO 里:

class Actor(mg.GeoAgent, _LinkNodeActor, BaseModelElement, ActorProtocol): ...
class BaseNature(BaseSubSystem, GeoSpace, NatureSystemProtocol): ...
class BaseHuman(BaseSubSystem, _LinkContainer, HumanSystemProtocol): ...
class BaseStateManager(ABC, StateManagerProtocol): ...
class BaseObservable(ABC, Observable): ...

Protocol 本该是零成本的结构化类型标注;一旦继承,它就变成了真实的 MRO 约束。最近一次提交 2c172159 "Swap ActorsList base order to fix mesa 3.5 MRO error" 就是这个设计的直接症状——上游改一次基类顺序,这里就得跟着调。

同一文件的其他误用:

  • container.pyagent_cls: Type[ActorProtocol] = ActorProtocol —— 把 Protocol 当运行时默认值,真去实例化会失败。
  • container.pyassert isinstance(agent, ActorProtocol) —— runtime_checkable 的 Protocol 只做结构性检查,任何长得像的对象都能通过,这个断言近乎空转(何况 assertpython -O 下会被剥掉)。
  • MainModelProtocol 里声明了带默认值的可变类属性——Protocol 不是干这个的。

建议:Protocol 只用于类型标注(if TYPE_CHECKING 下的注解、函数签名),不进任何 class X(...) 的基类列表。运行时需要的共享行为改用 ABC 或 mixin。这一步能显著降低未来 mesa 升级时的 MRO 风险。


3. human/links.py:三处方向分派逐字重复 + 为 Mock 写的静默兜底

同一段 in / out / None / else 阶梯在 owns_linksclean_links_oflinked 里出现三次,连错误信息都写了三个不同版本_LinkNodeCell._redirect_LinkNodeActor._redirect 函数体完全相同,只差一个字符串参数("agents" vs "at"),_target_is_me 同理。

更严重的是三处为测试替身让路的兜底,注释写得很直白:

# 如果是Mock对象或其他不可迭代对象,返回空列表
if hasattr(agents, "__iter__"): ...
else: ...

# 如果owning返回的不是可迭代对象,则假设没有链接
except TypeError:
    has_in = False
    has_out = False

if not isinstance(result, tuple):
    return (False, False)

这些会把真实的 TypeError 吞成「没有链接」——返回一个错误答案而不是抛异常。生产代码不该为 unittest.mock 的形状让步;正确做法是测试里用真实对象或专门的 fake。

顺带:_LinkProxy.hasnode is None 分支返回 (has_out, has_in),而调用方 unlinkhas_in, has_out = self.has(...) 解包——顺序是反的,值得一并核对。

建议:抽出一个 _pick_direction(direction) helper(统一错误信息),_redirect / _target_is_me 参数化掉那个字符串,删掉三处 Mock 兜底并相应修测试。


验收标准

  • create_module 在协议、抽象方法、BaseNatureBaseHuman 四处签名一致,并有一个跨子系统的参数化测试。
  • core/protocols.py 里的 Protocol 不再出现在任何类的基类列表中;isinstance(x, SomeProtocol) 不再用于校验。
  • links.py 的方向分派只有一处实现,错误信息统一。
  • links.py 里不再有针对 Mock 对象的分支;相关测试改用真实对象。
  • _LinkProxy.has 的返回顺序与所有调用方一致,并有测试固定。
  • 改动到的文件里,注释与 docstring 统一为英文。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ♻️ refactorCode restructuring without behaviour change

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions