Files
MoviePilot/scripts/architecture/event_facts.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

1835 lines
63 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
静态收集可证明的宿主 EventManager 的发布与消费事实。
"""
import ast
import hashlib
import json
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal, TypeAlias
# 省略优先级参数时使用固定标识,避免把默认值解析成动态表达式。
_DEFAULT_IDENTITY = "<default>"
# 无法静态解析的处理函数或优先级使用固定标识,不包含易变行号。
_DYNAMIC_IDENTITY = "<dynamic>"
_EVENT_MANAGER_METHODS = {"add_event_listener", "register"}
_PRODUCER_METHODS = {
"async_send_event",
"async_send_event_strict",
"send_event",
"send_event_strict",
}
_COMPREHENSION_SCOPES = (
ast.ListComp,
ast.SetComp,
ast.DictComp,
ast.GeneratorExp,
)
# 这些节点建立独立名称绑定边界,外层局部名称收集不得直接穿透。
_FUNCTION_SCOPES = (
ast.FunctionDef,
ast.AsyncFunctionDef,
ast.Lambda,
ast.ClassDef,
*_COMPREHENSION_SCOPES,
)
@dataclass(frozen=True, slots=True)
class _Symbol:
"""
记录规范来源模块、EventManager 类或实例的静态来源。
:param kind: 已确认的来源类别:模块、事件管理器类或工厂、管理器或发布器实例、注入字段所属对象、类型检查标记
:param value: 来源附加信息,保存规范模块或类路径、实例来源标记;无需附加信息时默认空字符串
"""
kind: Literal[
"module",
"manager_class",
"manager_factory",
"manager_instance",
"publisher_instance",
"injected_owner",
"type_checking",
]
value: str = ""
@dataclass(frozen=True, slots=True)
class _EventSelection:
"""
记录一次注册可静态确定的事件集合及未知余项。
:param events: 可静态确认的事件成员完整名称,按确定顺序保存
:param kind: 选择形式:member 为单个成员,enum 为枚举类,list 为事件列表
:param dynamic: 是否包含无法静态确认的事件部分,默认 False
:param invalid: 是否包含不符合注册契约的内容,默认 False
"""
events: tuple[str, ...]
kind: Literal["member", "enum", "list"]
dynamic: bool = False
invalid: bool = False
@dataclass(frozen=True, slots=True)
class _BoundEventMethod:
"""
记录来源已证明的调用对象的 EventManager 或 EventPublisher 绑定方法。
:param method: 已绑定的监听注册或事件发布方法名,仅接受声明中的六种方法
:param receiver_kind: 接收调用的对象来源类别,用于区分管理器实例、自身调用及注入端口
"""
method: Literal[
"add_event_listener",
"register",
"send_event",
"send_event_strict",
"async_send_event",
"async_send_event_strict",
]
receiver_kind: str
@dataclass(frozen=True, slots=True)
class _DecoratorFactory:
"""
记录尚未应用到处理函数的 EventManager.register 返回值。
:param selection: 装饰器工厂已绑定的事件选择结果
:param priority: 优先级表达式的稳定标识;省略时为 <default>,无法解析时为 <dynamic>
:param receiver_kind: 调用 register 的对象来源类别
"""
selection: _EventSelection
priority: str
receiver_kind: str
@dataclass(frozen=True, slots=True)
class _Registration:
"""
记录调用对象来源已确认的事件注册。
:param method: 注册入口名称,区分 add_event_listener 与 register
:param selection: 该次注册包含的静态事件及动态、无效标记
:param handler: 处理函数的稳定标识;匿名函数记为 <lambda>,无法解析时为 <dynamic>
:param priority: 优先级表达式的稳定标识;省略时为 <default>,无法解析时为 <dynamic>
:param receiver_kind: 发起注册的对象来源类别
"""
method: Literal["add_event_listener", "register"]
selection: _EventSelection
handler: str
priority: str
receiver_kind: str
_ScopeValue: TypeAlias = (
_Symbol | _EventSelection | _DecoratorFactory | _BoundEventMethod | None
)
def _expression_name(node: ast.AST | None) -> str:
"""
返回 Name/Attribute 表达式的稳定点分名称。
"""
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
prefix = _expression_name(node.value)
return ".".join(part for part in (prefix, node.attr) if part)
return ""
def _handler_identity(node: ast.AST | None) -> str:
"""
返回不含源码位置的处理函数标识。
"""
if name := _expression_name(node):
return name
if isinstance(node, ast.Lambda):
return "<lambda>"
return _DYNAMIC_IDENTITY
def _priority_identity(node: ast.AST | None) -> str:
"""
返回不含源码位置的优先级标识。
"""
if node is None:
return _DEFAULT_IDENTITY
if isinstance(node, ast.Constant):
return repr(node.value)
if name := _expression_name(node):
return name
return _DYNAMIC_IDENTITY
def fingerprint_event_fact(fact: Mapping[str, object]) -> str:
"""
计算排除诊断行号与已有摘要后的字段敏感 SHA256。
:param fact: 一条事件事实,line 与已有 fingerprint 不参与摘要
:return: 稳定 JSON 编码的 SHA256 十六进制摘要
"""
payload = {
key: value
for key, value in fact.items()
if key not in {"fingerprint", "line"}
}
encoded = json.dumps(
payload,
ensure_ascii=True,
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _fact_sort_key(item: dict[str, Any]) -> tuple[str, str, int, str]:
"""
返回逐调用事件事实的确定排序键。
"""
return (
str(item["caller"]),
str(item["qualname"]),
int(item["line"]),
str(item["fingerprint"]),
)
def _annotation_names(node: ast.AST | None) -> set[str]:
"""
提取类型注解中的有限点分名称。
"""
if node is None:
return set()
if isinstance(node, ast.Name):
return {node.id}
if isinstance(node, ast.Attribute):
return {_expression_name(node)}
return {
name
for child in ast.iter_child_nodes(node)
for name in _annotation_names(child)
}
def _bound_names(target: ast.AST) -> set[str]:
"""
返回赋值目标在当前词法作用域绑定的名称。
"""
if isinstance(target, ast.Name):
return {target.id}
if isinstance(target, (ast.List, ast.Tuple)):
return {
name
for element in target.elts
for name in _bound_names(element)
}
if isinstance(target, ast.Starred):
return _bound_names(target.value)
return set()
def _pattern_bound_names(pattern: ast.pattern) -> set[str]:
"""
返回结构化匹配模式捕获到当前作用域的名称。
"""
if isinstance(pattern, ast.MatchAs):
names = _pattern_bound_names(pattern.pattern) if pattern.pattern else set()
if pattern.name:
names.add(pattern.name)
return names
if isinstance(pattern, ast.MatchStar):
return {pattern.name} if pattern.name else set()
if isinstance(pattern, ast.MatchSequence):
return {
name
for child in pattern.patterns
for name in _pattern_bound_names(child)
}
if isinstance(pattern, ast.MatchMapping):
names = {
name
for child in pattern.patterns
for name in _pattern_bound_names(child)
}
if pattern.rest:
names.add(pattern.rest)
return names
if isinstance(pattern, ast.MatchClass):
return {
name
for child in (*pattern.patterns, *pattern.kwd_patterns)
for name in _pattern_bound_names(child)
}
if isinstance(pattern, ast.MatchOr):
return {
name
for child in pattern.patterns
for name in _pattern_bound_names(child)
}
return set()
def _function_local_names(
node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda,
) -> set[str]:
"""
按 Python 词法作用域预收集函数局部绑定,避免回退到同名全局。
"""
parents: dict[ast.AST, ast.AST] = {}
for parent in ast.walk(node):
for child in ast.iter_child_nodes(parent):
parents[child] = parent
def belongs_to_function(
candidate: ast.AST,
*,
cross_comprehensions: bool = False,
) -> bool:
"""
判断节点是否属于当前函数而非嵌套作用域。
"""
parent = parents.get(candidate)
while parent is not None and parent is not node:
if isinstance(parent, _FUNCTION_SCOPES):
if not (
cross_comprehensions
and isinstance(parent, _COMPREHENSION_SCOPES)
):
return False
parent = parents.get(parent)
return parent is node
local_names = {
argument.arg
for argument in (
*node.args.posonlyargs,
*node.args.args,
*node.args.kwonlyargs,
)
}
if node.args.vararg:
local_names.add(node.args.vararg.arg)
if node.args.kwarg:
local_names.add(node.args.kwarg.arg)
global_names: set[str] = set()
nonlocal_names: set[str] = set()
for candidate in ast.walk(node):
if candidate is node or not belongs_to_function(candidate):
continue
if isinstance(candidate, ast.Name) and isinstance(
candidate.ctx,
(ast.Store, ast.Del),
):
local_names.add(candidate.id)
elif isinstance(candidate, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
local_names.add(candidate.name)
elif isinstance(candidate, (ast.Import, ast.ImportFrom)):
for alias in candidate.names:
if alias.name != "*":
local_names.add(alias.asname or alias.name.split(".", 1)[0])
elif isinstance(candidate, ast.ExceptHandler) and candidate.name:
local_names.add(candidate.name)
elif isinstance(candidate, ast.Match):
local_names.update(
name
for case in candidate.cases
for name in _pattern_bound_names(case.pattern)
)
elif isinstance(candidate, ast.Global):
global_names.update(candidate.names)
elif isinstance(candidate, ast.Nonlocal):
nonlocal_names.update(candidate.names)
for candidate in ast.walk(node):
if isinstance(candidate, ast.NamedExpr) and belongs_to_function(
candidate,
cross_comprehensions=True,
):
local_names.update(_bound_names(candidate.target))
return local_names - global_names - nonlocal_names
def _event_port_symbol(
annotation: ast.AST | None,
canonical_aliases: Mapping[str, str] | None = None,
) -> _Symbol | None:
"""
把明确的 EventPublisher/EventManager 注解转换为调用对象来源。
"""
annotation_names = _annotation_names(annotation)
leaf_names = {name.rsplit(".", 1)[-1] for name in annotation_names}
if any(name.endswith("EventPublisher") for name in leaf_names):
return _Symbol("publisher_instance", "injected_event_publisher")
if any(name.endswith("EventManagerPort") for name in leaf_names):
return _Symbol("manager_instance", "injected_event_manager")
for name in annotation_names:
head, *tail = name.split(".")
canonical = ".".join(
((canonical_aliases or {}).get(head, head), *tail)
)
if canonical == "app.runtime.events.EventManager":
return _Symbol("manager_instance", "injected_event_manager")
return None
def _function_parameter_symbols(
node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda,
canonical_aliases: Mapping[str, str] | None = None,
) -> dict[str, _Symbol]:
"""
收集函数参数上明确声明的 Event 发布端口。
"""
arguments = (
*node.args.posonlyargs,
*node.args.args,
*node.args.kwonlyargs,
)
return {
argument.arg: symbol
for argument in arguments
if (
symbol := _event_port_symbol(
argument.annotation,
canonical_aliases,
)
) is not None
}
_InjectedFieldKey: TypeAlias = tuple[str, str]
def _resolve_import_from_module(
module_name: str,
node: ast.ImportFrom,
package_modules: set[str],
) -> str:
"""
把绝对或相对 from-import 解析为规范来源模块。
"""
if not node.level:
return node.module or ""
current_parts = module_name.split(".")
package_parts = (
current_parts
if module_name in package_modules
else current_parts[:-1]
)
ascend = node.level - 1
if ascend > len(package_parts):
return ""
base_parts = package_parts[:len(package_parts) - ascend]
if node.module:
base_parts.extend(node.module.split("."))
return ".".join(base_parts)
def _statement_bound_names(statement: ast.stmt) -> set[str]:
"""
返回模块语句直接或条件分支可能绑定的名称。
"""
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
return {statement.name}
if isinstance(statement, (ast.Assign, ast.AnnAssign, ast.AugAssign)):
targets = (
statement.targets
if isinstance(statement, ast.Assign)
else (statement.target,)
)
return {name for target in targets for name in _bound_names(target)}
if isinstance(statement, ast.Import):
return {item.asname or item.name.split(".", 1)[0] for item in statement.names}
if isinstance(statement, ast.ImportFrom):
return {item.asname or item.name for item in statement.names}
if isinstance(statement, ast.If):
return {
name
for branch in (statement.body, statement.orelse)
for child in branch
for name in _statement_bound_names(child)
}
return set()
def _runtime_module_statements(tree: ast.Module) -> list[ast.stmt]:
"""
展开可证明的 TYPE_CHECKING 运行期分支,保留其余顶层语句。
"""
type_checking_names: set[str] = set()
typing_modules: set[str] = set()
runtime_statements: list[ast.stmt] = []
def runtime_value(test: ast.expr) -> bool | None:
"""
判断表达式是否是当前未遮蔽的 typing.TYPE_CHECKING。
"""
negate = isinstance(test, ast.UnaryOp) and isinstance(test.op, ast.Not)
target = test.operand if negate else test
name = _expression_name(target)
proven = name in type_checking_names or any(
name == f"{module}.TYPE_CHECKING" for module in typing_modules
)
return negate if proven else None
def visit(statements: list[ast.stmt]) -> None:
"""
按模块执行顺序收集可证明会运行的语句。
"""
for statement in statements:
if isinstance(statement, ast.If):
value = runtime_value(statement.test)
if value is not None:
visit(statement.body if value else statement.orelse)
continue
rebound = _statement_bound_names(statement)
type_checking_names.difference_update(rebound)
typing_modules.difference_update(rebound)
runtime_statements.append(statement)
continue
rebound = _statement_bound_names(statement)
type_checking_names.difference_update(rebound)
typing_modules.difference_update(rebound)
runtime_statements.append(statement)
if isinstance(statement, ast.Import):
for item in statement.names:
if item.name == "typing":
typing_modules.add(item.asname or "typing")
elif isinstance(statement, ast.ImportFrom) and statement.module == "typing":
for item in statement.names:
if item.name == "TYPE_CHECKING":
type_checking_names.add(item.asname or item.name)
visit(tree.body)
return runtime_statements
def _module_import_aliases(
statements: list[ast.stmt],
*,
module_name: str,
package_modules: set[str],
) -> dict[str, str]:
"""
收集解析类继承关系所需的模块级导入别名。
"""
aliases: dict[str, str] = {}
for node in statements:
if isinstance(node, ast.Import):
for item in node.names:
bound = item.asname or item.name.split(".", 1)[0]
aliases[bound] = item.name if item.asname else bound
elif isinstance(node, ast.ImportFrom):
source_module = _resolve_import_from_module(
module_name,
node,
package_modules,
)
for item in node.names:
if item.name != "*":
canonical = ".".join(
part for part in (source_module, item.name) if part
)
aliases[item.asname or item.name] = canonical
return aliases
def _module_class_aliases(
statements: list[ast.stmt],
*,
module_name: str,
package_modules: set[str],
) -> dict[str, str]:
"""
收集运行期可证明的模块级简单类别名及其规范目标。
"""
aliases: dict[str, str] = {}
bindings: dict[str, str] = {}
def resolve(node: ast.expr) -> str:
"""
仅从当前未遮蔽绑定解析类别名右值。
"""
name = _expression_name(node)
if not name:
return ""
head, *tail = name.split(".")
if head not in bindings:
return ""
return ".".join((bindings[head], *tail))
for statement in statements:
bound_names = _statement_bound_names(statement)
target = (
resolve(statement.value)
if isinstance(statement, (ast.Assign, ast.AnnAssign))
and statement.value is not None
else ""
)
for name in bound_names:
bindings.pop(name, None)
aliases.pop(f"{module_name}.{name}", None)
if isinstance(statement, ast.Import):
for item in statement.names:
bound = item.asname or item.name.split(".", 1)[0]
bindings[bound] = item.name if item.asname else bound
continue
if isinstance(statement, ast.ImportFrom):
source_module = _resolve_import_from_module(
module_name,
statement,
package_modules,
)
for item in statement.names:
if item.name != "*":
canonical = ".".join(
part for part in (source_module, item.name) if part
)
bindings[item.asname or item.name] = canonical
continue
if isinstance(statement, ast.ClassDef):
bindings[statement.name] = f"{module_name}.{statement.name}"
continue
if isinstance(statement, ast.Assign):
targets = statement.targets
elif isinstance(statement, ast.AnnAssign) and statement.value is not None:
targets = (statement.target,)
else:
continue
target_names = [
target.id for target in targets if isinstance(target, ast.Name)
]
if len(target_names) != len(targets) or not target:
continue
for name in target_names:
bindings[name] = target
aliases[f"{module_name}.{name}"] = target
return aliases
def _iter_classes(
statements: list[ast.stmt],
prefix: tuple[str, ...] = (),
) -> list[tuple[str, ast.ClassDef]]:
"""
按词法限定名返回语句中的类定义,不进入函数作用域。
"""
classes: list[tuple[str, ast.ClassDef]] = []
for statement in statements:
if not isinstance(statement, ast.ClassDef):
continue
qualname = ".".join((*prefix, statement.name))
classes.append((qualname, statement))
classes.extend(_iter_classes(statement.body, (*prefix, statement.name)))
return classes
def _canonical_class_name(
node: ast.expr,
*,
module_name: str,
aliases: dict[str, str],
) -> str:
"""
把有限 Name/Attribute 基类表达式还原成规范类名。
"""
name = _expression_name(node)
if not name:
return ""
head, *tail = name.split(".")
if head in aliases:
return ".".join((aliases[head], *tail))
return f"{module_name}.{name}"
def _discover_injected_event_fields(
trees: dict[str, ast.Module],
package_modules: set[str],
) -> dict[_InjectedFieldKey, dict[str, _Symbol]]:
"""
按所属类发现构造注入字段,并沿已知继承关系传播。
"""
runtime_statements = {
module_name: _runtime_module_statements(tree)
for module_name, tree in trees.items()
}
classes = {
f"{module_name}.{qualname}": (module_name, qualname, node)
for module_name, statements in runtime_statements.items()
for qualname, node in _iter_classes(statements)
}
aliases = {
module_name: _module_import_aliases(
runtime_statements[module_name],
module_name=module_name,
package_modules=package_modules,
)
for module_name in trees
}
class_aliases = {
alias: target
for module_name, statements in runtime_statements.items()
for alias, target in _module_class_aliases(
statements,
module_name=module_name,
package_modules=package_modules,
).items()
}
def resolve_class_alias(canonical: str) -> str:
"""
传递解析简单类别名,并拒绝循环别名。
"""
visited: set[str] = set()
while canonical in class_aliases and canonical not in visited:
visited.add(canonical)
canonical = class_aliases[canonical]
return "" if canonical in visited else canonical
fields: dict[str, dict[str, _Symbol]] = {
canonical: {} for canonical in classes
}
bases_by_class: dict[str, set[str]] = {
canonical: set() for canonical in classes
}
descendants_by_class: dict[str, set[str]] = {
canonical: set() for canonical in classes
}
for canonical, (module_name, _qualname, class_node) in classes.items():
for base in class_node.bases:
base_name = _canonical_class_name(
base,
module_name=module_name,
aliases=aliases[module_name],
)
base_name = resolve_class_alias(base_name)
if base_name in classes:
bases_by_class[canonical].add(base_name)
descendants_by_class[base_name].add(canonical)
for canonical, (module_name, qualname, class_node) in classes.items():
class_fields = fields[canonical]
for constructor in (
node
for node in class_node.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == "__init__"
):
parameters = _function_parameter_symbols(
constructor,
aliases[module_name],
)
for candidate in constructor.body:
candidates = ast.walk(candidate) if not isinstance(
candidate,
(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef),
) else ()
for assignment in candidates:
if isinstance(assignment, ast.Assign):
pairs = (
(target, assignment.value)
for target in assignment.targets
)
elif isinstance(assignment, ast.AnnAssign) and assignment.value:
pairs = ((assignment.target, assignment.value),)
else:
continue
for target, value in pairs:
if not (
isinstance(target, ast.Attribute)
and isinstance(target.value, ast.Name)
and target.value.id == "self"
):
continue
symbol = (
parameters.get(value.id)
if isinstance(value, ast.Name)
else None
)
if (
symbol is None
and module_name == "app.chain.base"
and qualname == "ChainBase"
and target.attr == "eventmanager"
and isinstance(value, ast.Attribute)
and value.attr == "event_manager"
):
symbol = _Symbol(
"manager_instance",
"injected_event_manager",
)
if symbol is not None:
class_fields[target.attr] = symbol
direct_fields = {
canonical: dict(class_fields)
for canonical, class_fields in fields.items()
if class_fields
}
for owner, owner_fields in direct_fields.items():
def reachable(
starts: set[str],
relations: dict[str, set[str]],
) -> set[str]:
"""
返回给定继承方向上的传递闭包。
"""
pending = list(starts)
reached: set[str] = set()
while pending:
current = pending.pop()
if current in reached:
continue
reached.add(current)
pending.extend(relations[current])
return reached
descendants = reachable({owner}, descendants_by_class)
targets = set(descendants)
targets.update(reachable(descendants, bases_by_class))
for target in targets:
fields[target].update(owner_fields)
return {
(module_name, qualname): fields[canonical]
for canonical, (module_name, qualname, _node) in classes.items()
if fields[canonical]
}
class _EventFactCollector(ast.NodeVisitor):
"""
以有限词法来源追踪收集单个宿主模块的事件事实。
"""
def __init__(
self,
module_name: str,
event_members: dict[str, tuple[str, ...]],
*,
collect_facts: bool,
module_final_scope: dict[str, _ScopeValue] | None = None,
injected_fields: dict[_InjectedFieldKey, dict[str, _Symbol]] | None = None,
) -> None:
"""
初始化模块、事件枚举、收集模式及已证明的注入字段。
"""
self._module_name = module_name
self._event_members = event_members
self._collect_facts = collect_facts
self._module_final_scope = module_final_scope or {}
self._injected_fields = injected_fields or {}
self._scopes: list[dict[str, _ScopeValue]] = [{}]
self._scope_kinds = ["module"]
self._function_final_scopes: list[dict[str, _ScopeValue]] = []
self._qualnames: list[str] = []
self._class_qualnames: list[str] = []
self.producers: list[dict[str, Any]] = []
self.consumers: list[dict[str, Any]] = []
def module_scope(self) -> dict[str, _ScopeValue]:
"""
返回按模块执行顺序收敛后的符号状态。
"""
return dict(self._scopes[0])
def _lookup(self, name: str) -> _ScopeValue:
"""
从内向外解析词法绑定。
"""
skip_class_scope = self._scope_kinds[-1] == "comprehension"
for scope, kind in reversed(list(zip(self._scopes, self._scope_kinds))):
if skip_class_scope and kind == "class":
continue
if name in scope:
return scope[name]
return None
def _set(self, name: str, value: _ScopeValue) -> None:
"""
在当前词法作用域写入或清空绑定。
"""
self._scopes[-1][name] = value
@staticmethod
def _merge_scope_states(
states: list[list[dict[str, _ScopeValue]]],
) -> list[dict[str, _ScopeValue]]:
"""
仅保留所有控制流路径一致的静态来源信息。
"""
return [
{
name: (
scopes[0].get(name)
if all(
scope.get(name) == scopes[0].get(name)
for scope in scopes[1:]
)
else None
)
for name in set().union(*(scope.keys() for scope in scopes))
}
for scopes in zip(*states)
]
def _discover_scope_after(
self,
statements: list[ast.stmt],
scopes: list[dict[str, _ScopeValue]],
scope_kinds: list[str],
) -> dict[str, _ScopeValue]:
"""
无事实副作用地计算一组语句执行后的最内层作用域。
"""
discovery = _EventFactCollector(
self._module_name,
self._event_members,
collect_facts=False,
module_final_scope=self._module_final_scope,
injected_fields=self._injected_fields,
)
discovery._scopes = [dict(scope) for scope in scopes]
discovery._scope_kinds = list(scope_kinds)
discovery._qualnames = list(self._qualnames)
discovery._class_qualnames = list(self._class_qualnames)
for statement in statements:
discovery.visit(statement)
return dict(discovery._scopes[-1])
def _function_initial_scope(
self,
node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda,
) -> dict[str, _ScopeValue]:
"""
构造函数调用期的局部符号,并保留明确的参数端口来源信息。
"""
scope: dict[str, _ScopeValue] = {
name: None for name in _function_local_names(node)
}
canonical_aliases = {
name: "app.runtime.events.EventManager"
for name, value in self._module_final_scope.items()
if isinstance(value, _Symbol) and value.kind == "manager_class"
}
if (
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == "__init__"
and self._class_qualnames
):
scope.update(_function_parameter_symbols(node, canonical_aliases))
positional = (*node.args.posonlyargs, *node.args.args)
if (
self._module_name == "app.runtime.events"
and self._qualnames == ["EventManager"]
and positional
and positional[0].arg == "self"
):
scope["self"] = _Symbol("publisher_instance", "event_manager_self")
elif (
self._class_qualnames
and positional
and not any(
_expression_name(decorator) in {"classmethod", "staticmethod"}
for decorator in getattr(node, "decorator_list", ())
)
and (
self._module_name,
self._class_qualnames[-1],
) in self._injected_fields
):
scope[positional[0].arg] = _Symbol(
"injected_owner",
self._class_qualnames[-1],
)
return scope
def _symbol_for_canonical(self, canonical: str) -> _ScopeValue:
"""
把已知规范路径转换为收集器使用的符号。
"""
if canonical == "app.runtime.events.eventmanager":
return _Symbol("manager_instance", "canonical_singleton")
if canonical == "app.runtime.events.EventManager":
return _Symbol("manager_class")
if canonical == "typing.TYPE_CHECKING":
return _Symbol("type_checking")
if canonical in {
"app",
"app.runtime",
"app.runtime.events",
"app.schemas",
"app.schemas.types",
"typing",
}:
return _Symbol("module", canonical)
for enum_name, members in self._event_members.items():
if canonical == f"app.schemas.types.{enum_name}":
return _EventSelection(
events=tuple(f"{enum_name}.{member}" for member in members),
kind="enum",
)
return None
def _runtime_type_checking_value(self, test: ast.expr) -> bool | None:
"""
仅对可证明的 typing.TYPE_CHECKING 返回确定运行期分支。
"""
negate = isinstance(test, ast.UnaryOp) and isinstance(test.op, ast.Not)
target = test.operand if negate else test
resolved = self._resolve(target)
if isinstance(resolved, _Symbol) and resolved.kind == "type_checking":
return negate
return None
def _resolve(self, node: ast.AST) -> _ScopeValue:
"""
解析已知导入、模块和赋值别名,不跨函数推断返回值。
"""
if isinstance(node, ast.Name):
return self._lookup(node.id)
if isinstance(node, ast.Attribute):
parent = self._resolve(node.value)
if isinstance(parent, _Symbol) and parent.kind == "injected_owner":
fields = self._injected_fields.get(
(self._module_name, parent.value),
{},
)
if node.attr in fields:
return fields[node.attr]
if isinstance(parent, _Symbol) and parent.kind == "module":
return self._symbol_for_canonical(f"{parent.value}.{node.attr}")
if isinstance(parent, _Symbol) and parent.kind == "manager_class":
if node.attr == "get_existing_instance":
return _Symbol("manager_factory")
if isinstance(parent, _Symbol) and parent.kind in {
"manager_instance",
"publisher_instance",
}:
methods = (
_PRODUCER_METHODS | _EVENT_MANAGER_METHODS
if parent.kind == "manager_instance"
else _PRODUCER_METHODS
)
if node.attr in methods:
return _BoundEventMethod(node.attr, parent.value)
if isinstance(parent, _EventSelection) and parent.kind == "enum":
enum_name = parent.events[0].split(".", 1)[0] if parent.events else ""
if node.attr in self._event_members.get(enum_name, ()):
return _EventSelection(
events=(f"{enum_name}.{node.attr}",),
kind="member",
)
return _EventSelection((), "member", invalid=True)
return None
if isinstance(node, ast.List):
return self._resolve_event_selection(
node,
allow_enum=True,
allow_list=True,
)
if isinstance(node, ast.IfExp):
return self._resolve_event_selection(
node,
allow_enum=False,
allow_list=False,
)
if isinstance(node, ast.Call):
target = self._resolve(node.func)
if isinstance(target, _Symbol) and target.kind in {
"manager_class",
"manager_factory",
}:
receiver_kind = (
"constructed_manager"
if target.kind == "manager_class"
else "existing_manager"
)
return _Symbol("manager_instance", receiver_kind)
registration = self._registration(node)
if registration and registration.method == "register":
return _DecoratorFactory(
selection=registration.selection,
priority=registration.priority,
receiver_kind=registration.receiver_kind,
)
return None
def _resolve_event_selection(
self,
node: ast.AST,
*,
allow_enum: bool,
allow_list: bool,
) -> _EventSelection:
"""
解析单个事件、枚举类或 register 接受的静态列表。
"""
if isinstance(node, ast.List):
if not allow_list:
return _EventSelection((), "list", dynamic=True)
selections = [
self._resolve_event_selection(
item,
allow_enum=True,
allow_list=False,
)
for item in node.elts
]
return _EventSelection(
events=tuple(sorted({event for item in selections for event in item.events})),
kind="list",
dynamic=any(item.dynamic for item in selections),
invalid=any(item.invalid for item in selections),
)
if isinstance(node, ast.IfExp):
branches = (
self._resolve_event_selection(
node.body,
allow_enum=allow_enum,
allow_list=allow_list,
),
self._resolve_event_selection(
node.orelse,
allow_enum=allow_enum,
allow_list=allow_list,
),
)
return _EventSelection(
events=tuple(sorted({event for item in branches for event in item.events})),
kind="list" if allow_list else "member",
dynamic=any(item.dynamic for item in branches),
invalid=any(item.invalid for item in branches),
)
resolved = self._resolve(node)
if isinstance(resolved, _EventSelection):
if resolved.kind == "member":
return resolved
if resolved.kind == "enum" and allow_enum:
return resolved
if resolved.kind == "list" and allow_list:
return resolved
return _EventSelection((), "member", dynamic=True)
@staticmethod
def _bind_call_arguments(
node: ast.Call,
parameter_names: tuple[str, ...],
required_names: frozenset[str],
) -> dict[str, ast.AST] | None:
"""
按真实 Python 调用规则绑定有限签名,拒绝未知或重复参数。
"""
if len(node.args) > len(parameter_names) or any(
isinstance(argument, ast.Starred) for argument in node.args
):
return None
arguments = dict(zip(parameter_names, node.args))
for keyword in node.keywords:
if (
keyword.arg is None
or keyword.arg not in parameter_names
or keyword.arg in arguments
):
return None
arguments[keyword.arg] = keyword.value
if not required_names.issubset(arguments):
return None
return arguments
def _registration(self, node: ast.Call) -> _Registration | None:
"""
仅解析调用对象来源已证明为规范 EventManager 实例的注册。
"""
bound_method = self._resolve(node.func)
if not (
isinstance(bound_method, _BoundEventMethod)
and bound_method.method in _EVENT_MANAGER_METHODS
):
return None
method = bound_method.method
if method == "register":
arguments = self._bind_call_arguments(
node,
("etype", "priority"),
frozenset({"etype"}),
)
event_name = "etype"
handler_node = None
else:
arguments = self._bind_call_arguments(
node,
("event_type", "handler", "priority"),
frozenset({"event_type", "handler"}),
)
event_name = "event_type"
handler_node = arguments.get("handler") if arguments else None
if arguments is None:
return None
selection = self._resolve_event_selection(
arguments[event_name],
allow_enum=method == "register",
allow_list=method == "register",
)
return _Registration(
method=method,
selection=selection,
handler=_handler_identity(handler_node),
priority=_priority_identity(arguments.get("priority")),
receiver_kind=bound_method.receiver_kind,
)
def _producer_call(
self,
node: ast.Call,
) -> tuple[_BoundEventMethod, _EventSelection] | None:
"""
解析调用对象与调用签名均可证明的事件发布。
"""
bound_method = self._resolve(node.func)
if not (
isinstance(bound_method, _BoundEventMethod)
and bound_method.method in _PRODUCER_METHODS
):
return None
arguments = self._bind_call_arguments(
node,
("etype", "data", "priority"),
frozenset({"etype"}),
)
if arguments is None:
return None
return (
bound_method,
self._resolve_event_selection(
arguments["etype"],
allow_enum=False,
allow_list=False,
),
)
def _decorator_factory_application(
self,
node: ast.Call,
) -> tuple[_DecoratorFactory, ast.AST] | None:
"""
解析合法的 decorator(f) 或 decorator(f=...) 立即应用。
"""
factory = self._resolve(node.func)
if not isinstance(factory, _DecoratorFactory):
return None
arguments = self._bind_call_arguments(
node,
("f",),
frozenset({"f"}),
)
if arguments is None:
return None
return factory, arguments["f"]
def _base_fact(
self,
node: ast.AST,
selection: _EventSelection,
*,
method: str,
receiver_kind: str,
) -> dict[str, Any]:
"""
构造带不含行号的指纹的逐调用基础事实。
"""
fact = {
"caller": self._module_name,
"line": node.lineno,
"qualname": ".".join(self._qualnames) or "<module>",
"method": method,
"receiver_kind": receiver_kind,
"events": list(selection.events),
"dynamic": selection.dynamic,
"invalid": selection.invalid,
}
fact["fingerprint"] = fingerprint_event_fact(fact)
return fact
def _record_consumer(
self,
node: ast.AST,
selection: _EventSelection,
*,
method: str,
receiver_kind: str,
handler: str,
priority: str,
registration_kind: Literal["decorator", "listener"],
) -> None:
"""
记录包含处理函数、注册类型和优先级的完整事件消费事实。
"""
if not self._collect_facts:
return
fact = self._base_fact(
node,
selection,
method=method,
receiver_kind=receiver_kind,
)
fact.update({
"handler": handler,
"priority": priority,
"registration_kind": registration_kind,
})
fact["fingerprint"] = fingerprint_event_fact(fact)
self.consumers.append(fact)
def _record_producer(
self,
node: ast.Call,
method: _BoundEventMethod,
selection: _EventSelection,
) -> None:
"""
写入一条调用对象来源已证明的事件发布事实。
"""
if not self._collect_facts:
return
self.producers.append(
self._base_fact(
node,
selection,
method=method.method,
receiver_kind=method.receiver_kind,
)
)
def _record_decorator(self, decorator: ast.expr, handler: str) -> bool:
"""
记录直接或简单赋值别名形式的 register 装饰器。
"""
factory = self._resolve(decorator)
if not isinstance(factory, _DecoratorFactory):
return False
self._record_consumer(
decorator,
factory.selection,
method="register",
receiver_kind=factory.receiver_kind,
handler=handler,
priority=factory.priority,
registration_kind="decorator",
)
return True
def _visit_definition_decorators(
self,
decorators: list[ast.expr],
handler: str,
) -> None:
"""
按定义期作用域访问装饰器,事件注册只在实际应用时记账。
"""
for decorator in decorators:
if not self._record_decorator(decorator, handler):
self.visit(decorator)
def visit_Import(self, node: ast.Import) -> None:
"""
发布模块导入及其别名。
"""
for alias in node.names:
bound_name = alias.asname or alias.name.split(".", 1)[0]
canonical = alias.name if alias.asname else bound_name
self._set(bound_name, self._symbol_for_canonical(canonical))
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
"""
发布规范来源的 from-import 及其别名。
"""
if node.level or not node.module:
for alias in node.names:
if alias.name != "*":
self._set(alias.asname or alias.name, None)
return
for alias in node.names:
if alias.name == "*":
continue
self._set(
alias.asname or alias.name,
self._symbol_for_canonical(f"{node.module}.{alias.name}"),
)
def visit_Assign(self, node: ast.Assign) -> None:
"""
按执行顺序传播或清空简单赋值别名。
"""
value = self._resolve(node.value)
self.visit(node.value)
for target in node.targets:
for name in _bound_names(target):
self._set(name, value)
def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
"""
传播带注解且有值的简单赋值别名。
"""
value = self._resolve(node.value) if node.value is not None else None
if node.value is not None:
self.visit(node.value)
for name in _bound_names(node.target):
self._set(name, value)
def visit_AugAssign(self, node: ast.AugAssign) -> None:
"""
增量赋值使目标来源信息失效。
"""
self.visit(node.value)
for name in _bound_names(node.target):
self._set(name, None)
def visit_NamedExpr(self, node: ast.NamedExpr) -> None:
"""
传播海象表达式的简单别名。
"""
value = self._resolve(node.value)
self.visit(node.value)
scope_index = len(self._scopes) - 1
while self._scope_kinds[scope_index] == "comprehension":
scope_index -= 1
for name in _bound_names(node.target):
self._scopes[scope_index][name] = value
def visit_Delete(self, node: ast.Delete) -> None:
"""
删除名称后清空其来源信息。
"""
for target in node.targets:
for name in _bound_names(target):
self._set(name, None)
def visit_If(self, node: ast.If) -> None:
"""
精确执行 TYPE_CHECKING 分支,其余条件保守合并绑定。
"""
self.visit(node.test)
runtime_value = self._runtime_type_checking_value(node.test)
if runtime_value is not None:
statements = node.body if runtime_value else node.orelse
for statement in statements:
self.visit(statement)
return
original = [dict(scope) for scope in self._scopes]
for statement in node.body:
self.visit(statement)
body_scopes = [dict(scope) for scope in self._scopes]
self._scopes = [dict(scope) for scope in original]
for statement in node.orelse:
self.visit(statement)
else_scopes = [dict(scope) for scope in self._scopes]
self._scopes = self._merge_scope_states([body_scopes, else_scopes])
def visit_For(self, node: ast.For) -> None:
"""
让循环目标遮蔽旧绑定,并保守合并零次与迭代路径。
"""
self._visit_for(node)
def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
"""
按同步循环相同规则处理异步循环目标。
"""
self._visit_for(node)
def _visit_for(self, node: ast.For | ast.AsyncFor) -> None:
"""
实现同步和异步循环共享的来源信息分析。
"""
self.visit(node.iter)
original = [dict(scope) for scope in self._scopes]
for name in _bound_names(node.target):
self._set(name, None)
for statement in node.body:
self.visit(statement)
body_scopes = [dict(scope) for scope in self._scopes]
loop_exit = self._merge_scope_states([original, body_scopes])
self._scopes = [dict(scope) for scope in loop_exit]
for statement in node.orelse:
self.visit(statement)
else_scopes = [dict(scope) for scope in self._scopes]
self._scopes = self._merge_scope_states([loop_exit, else_scopes])
def visit_While(self, node: ast.While) -> None:
"""
保守合并 while 的零次、迭代和 else 路径。
"""
self.visit(node.test)
original = [dict(scope) for scope in self._scopes]
for statement in node.body:
self.visit(statement)
body_scopes = [dict(scope) for scope in self._scopes]
loop_exit = self._merge_scope_states([original, body_scopes])
self._scopes = [dict(scope) for scope in loop_exit]
for statement in node.orelse:
self.visit(statement)
else_scopes = [dict(scope) for scope in self._scopes]
self._scopes = self._merge_scope_states([loop_exit, else_scopes])
def visit_With(self, node: ast.With) -> None:
"""
按进入顺序分析上下文管理器,并清空 with 目标的旧来源信息。
"""
self._visit_with(node)
def visit_AsyncWith(self, node: ast.AsyncWith) -> None:
"""
按同步 with 相同规则处理异步上下文目标。
"""
self._visit_with(node)
def _visit_with(self, node: ast.With | ast.AsyncWith) -> None:
"""
实现同步和异步上下文管理器共享的绑定分析。
"""
for item in node.items:
self.visit(item.context_expr)
if item.optional_vars is not None:
for name in _bound_names(item.optional_vars):
self._set(name, None)
for statement in node.body:
self.visit(statement)
def visit_Match(self, node: ast.Match) -> None:
"""
隔离 match case 捕获名称并保守合并所有匹配路径。
"""
self.visit(node.subject)
original = [dict(scope) for scope in self._scopes]
states = [original]
for case in node.cases:
self._scopes = [dict(scope) for scope in original]
self.visit(case.pattern)
for name in _pattern_bound_names(case.pattern):
self._set(name, None)
if case.guard is not None:
self.visit(case.guard)
for statement in case.body:
self.visit(statement)
states.append([dict(scope) for scope in self._scopes])
self._scopes = self._merge_scope_states(states)
def visit_Try(self, node: ast.Try) -> None:
"""
隔离异常处理路径,并让异常别名在异常处理体内外失效。
"""
self._visit_try(node)
def visit_TryStar(self, node: ast.TryStar) -> None:
"""
按普通 try 相同规则处理异常组分支。
"""
self._visit_try(node)
def _visit_try(self, node: ast.Try | ast.TryStar) -> None:
"""
实现 try 与 try-star 共享的保守控制流合并。
"""
original = [dict(scope) for scope in self._scopes]
exception_states = [original]
for statement in node.body:
exception_states.append([dict(scope) for scope in self._scopes])
self.visit(statement)
exception_states.append([dict(scope) for scope in self._scopes])
exception_entry = self._merge_scope_states(exception_states)
for statement in node.orelse:
self.visit(statement)
continuing_states = [[dict(scope) for scope in self._scopes]]
for handler in node.handlers:
self._scopes = [dict(scope) for scope in exception_entry]
if handler.type is not None:
self.visit(handler.type)
if handler.name:
self._set(handler.name, None)
for statement in handler.body:
self.visit(statement)
if handler.name:
self._set(handler.name, None)
continuing_states.append([dict(scope) for scope in self._scopes])
self._scopes = self._merge_scope_states(
[*continuing_states, exception_entry]
if node.finalbody
else continuing_states
)
for statement in node.finalbody:
self.visit(statement)
def _visit_comprehension(
self,
generators: list[ast.comprehension],
expressions: tuple[ast.expr, ...],
) -> None:
"""
在独立词法作用域内分析推导式目标和表达式。
"""
first, *remaining = generators
self.visit(first.iter)
outer_original = [dict(scope) for scope in self._scopes]
self._scopes.append({})
self._scope_kinds.append("comprehension")
for name in _bound_names(first.target):
self._set(name, None)
for condition in first.ifs:
self.visit(condition)
for generator in remaining:
self.visit(generator.iter)
for name in _bound_names(generator.target):
self._set(name, None)
for condition in generator.ifs:
self.visit(condition)
for expression in expressions:
self.visit(expression)
self._scope_kinds.pop()
self._scopes.pop()
outer_after = [dict(scope) for scope in self._scopes]
self._scopes = self._merge_scope_states([outer_original, outer_after])
def visit_ListComp(self, node: ast.ListComp) -> None:
"""
在独立作用域内分析列表推导式。
"""
self._visit_comprehension(node.generators, (node.elt,))
def visit_SetComp(self, node: ast.SetComp) -> None:
"""
在独立作用域内分析集合推导式。
"""
self._visit_comprehension(node.generators, (node.elt,))
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> None:
"""
在独立作用域内分析生成器表达式。
"""
self._visit_comprehension(node.generators, (node.elt,))
def visit_DictComp(self, node: ast.DictComp) -> None:
"""
在独立作用域内分析字典推导式。
"""
self._visit_comprehension(node.generators, (node.key, node.value))
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
"""
按定义期装饰器和调用期词法作用域分别分析函数。
"""
self._visit_function(node)
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
"""
按普通函数相同规则分析异步函数。
"""
self._visit_function(node)
def _visit_function(
self,
node: ast.FunctionDef | ast.AsyncFunctionDef,
) -> None:
"""
实现同步和异步函数共享的作用域分析。
"""
if not self._collect_facts:
self._set(node.name, None)
return
handler = ".".join((*self._qualnames, node.name))
self._visit_definition_decorators(node.decorator_list, handler)
for expression in (
*node.args.defaults,
*(default for default in node.args.kw_defaults if default is not None),
):
self.visit(expression)
saved_scopes = self._scopes
saved_kinds = self._scope_kinds
body_scopes = [
dict(self._module_final_scope),
*(dict(scope) for scope in self._function_final_scopes),
self._function_initial_scope(node),
]
body_kinds = [
"module",
*("function" for _ in self._function_final_scopes),
"function",
]
final_scope = self._discover_scope_after(node.body, body_scopes, body_kinds)
self._scopes = body_scopes
self._scope_kinds = body_kinds
self._function_final_scopes.append(final_scope)
self._qualnames.append(node.name)
for statement in node.body:
self.visit(statement)
self._qualnames.pop()
self._function_final_scopes.pop()
self._scopes = saved_scopes
self._scope_kinds = saved_kinds
self._set(node.name, None)
def visit_ClassDef(self, node: ast.ClassDef) -> None:
"""
在类定义期作用域记录类装饰器和方法装饰器。
"""
if not self._collect_facts:
self._set(node.name, None)
return
handler = ".".join((*self._qualnames, node.name))
self._visit_definition_decorators(node.decorator_list, handler)
for expression in (*node.bases, *(keyword.value for keyword in node.keywords)):
self.visit(expression)
self._qualnames.append(node.name)
self._class_qualnames.append(".".join(self._qualnames))
self._scopes.append({})
self._scope_kinds.append("class")
for statement in node.body:
self.visit(statement)
self._scope_kinds.pop()
self._scopes.pop()
self._class_qualnames.pop()
self._qualnames.pop()
self._set(node.name, None)
def visit_Lambda(self, node: ast.Lambda) -> None:
"""
让 lambda 参数遮蔽同名模块别名。
"""
if not self._collect_facts:
return
for expression in (
*node.args.defaults,
*(default for default in node.args.kw_defaults if default is not None),
):
self.visit(expression)
saved_scopes = self._scopes
saved_kinds = self._scope_kinds
body_scopes = [
dict(self._module_final_scope),
*(dict(scope) for scope in self._function_final_scopes),
self._function_initial_scope(node),
]
body_kinds = [
"module",
*("function" for _ in self._function_final_scopes),
"function",
]
final_scope = self._discover_scope_after(
[ast.Expr(value=node.body)],
body_scopes,
body_kinds,
)
self._scopes = body_scopes
self._scope_kinds = body_kinds
self._function_final_scopes.append(final_scope)
self.visit(node.body)
self._function_final_scopes.pop()
self._scopes = saved_scopes
self._scope_kinds = saved_kinds
def visit_Call(self, node: ast.Call) -> None:
"""
记录调用对象与调用签名均可证明的事件发布或消费。
"""
producer = self._producer_call(node)
if producer is not None:
method, selection = producer
self._record_producer(node, method, selection)
factory_application = self._decorator_factory_application(node)
if factory_application:
factory, handler_node = factory_application
self._record_consumer(
node,
factory.selection,
method="register",
receiver_kind=factory.receiver_kind,
handler=_handler_identity(handler_node),
priority=factory.priority,
registration_kind="decorator",
)
else:
registration = self._registration(node)
if registration and registration.method == "add_event_listener":
self._record_consumer(
node,
registration.selection,
method="add_event_listener",
receiver_kind=registration.receiver_kind,
handler=registration.handler,
priority=registration.priority,
registration_kind="listener",
)
self.generic_visit(node)
def collect_event_facts(
modules: dict[str, Path],
event_members: dict[str, tuple[str, ...]],
) -> dict[str, list[dict[str, Any]]]:
"""
收集宿主 EventManager 的逐调用事件发布与消费事实。
只有可追溯到规范 EventManager、其内部 ``self`` 或明确注入事件端口的
调用才进入事实。插件模块与未知同名调用对象始终忽略;每条事实携带排除行号
的字段敏感 SHA256,供基线稳定追踪。
:param modules: 宿主模块名到 Python 源码路径的映射
:param event_members: EventType/ChainEventType 到公开成员名的映射
:return: ``producers`` 与 ``consumers`` 两组稳定排序的逐调用事实
"""
trees = {
module_name: ast.parse(
path.read_text(encoding="utf-8-sig"),
filename=str(path),
)
for module_name, path in sorted(modules.items())
if module_name != "app.plugins"
and not module_name.startswith("app.plugins.")
}
package_modules = {
module_name
for module_name, path in modules.items()
if path.name == "__init__.py"
}
injected_fields = _discover_injected_event_fields(
trees,
package_modules,
)
producers: list[dict[str, Any]] = []
consumers: list[dict[str, Any]] = []
for module_name, tree in sorted(trees.items()):
discovery = _EventFactCollector(
module_name,
event_members,
collect_facts=False,
injected_fields=injected_fields,
)
discovery.visit(tree)
collector = _EventFactCollector(
module_name,
event_members,
collect_facts=True,
module_final_scope=discovery.module_scope(),
injected_fields=injected_fields,
)
collector.visit(tree)
producers.extend(collector.producers)
consumers.extend(collector.consumers)
return {
"producers": sorted(producers, key=_fact_sort_key),
"consumers": sorted(consumers, key=_fact_sort_key),
}