Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions astrbot/core/utils/pip_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,10 +619,34 @@ def _is_module_loaded_from_site_packages(
return False


def _has_loaded_c_extension(module_name: str) -> bool:
"""检查 sys.modules 中目标模块的依赖子树是否已包含 C 扩展。

遍历已加载的 key(如 'pikepdf'、'pikepdf._core'),
检查其 __file__ 后缀是否为 .pyd / .so。
"""
for key in list(sys.modules.keys()):
if not (key == module_name or key.startswith(f"{module_name}.")):
continue
mod = sys.modules.get(key)
if mod is None:
continue
mod_file = getattr(mod, "__file__", "") or ""
if os.path.splitext(mod_file)[1].lower() in (".pyd", ".so"):
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
return True
Comment on lines +634 to +636

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了使 C 扩展检测更加健壮和具有防御性,我们应该显式检查 __file__ 是否为字符串。在某些环境(如命名空间包、冻结模块或测试中的 Mock 模块)中,__file__ 可能是 None 甚至是非字符串类型。使用 isinstance(mod_file, str) 结合 .endswith() 更加安全、简洁,并且可以避免在路径解析过程中可能出现的 TypeErrorAttributeError

Suggested change
mod_file = getattr(mod, "__file__", "") or ""
if os.path.splitext(mod_file)[1].lower() in (".pyd", ".so"):
return True
mod_file = getattr(mod, "__file__", None)
if isinstance(mod_file, str) and mod_file.lower().endswith((".pyd", ".so")):
return True

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against the live AstrBot process: every module in sys.modules has __file__ as str (24 modules) or None (22 modules) — zero instances of int, Path, or other non-str types. CPython guarantees __file__ is str | None on real modules. Our or "" fallback already converts None to an empty string, which yields a non-matching suffix from splitext. The isinstance check covers a scenario that cannot occur in production module introspection, so we're keeping the simpler or "" guard.

return False


def _prefer_module_from_site_packages(
module_name: str, site_packages_path: str
) -> bool:
with _SITE_PACKAGES_IMPORT_LOCK:
if _has_loaded_c_extension(module_name):
logger.debug(
"Skipping prefer for %s: C extension detected in submodules",
module_name,
)
return False
base_path = os.path.join(site_packages_path, *module_name.split("."))
package_init = os.path.join(base_path, "__init__.py")
module_file = f"{base_path}.py"
Expand All @@ -646,6 +670,8 @@ def _prefer_module_from_site_packages(
if spec is None or spec.loader is None:
return False

# 已在 _has_loaded_c_extension 中扫描过——此处收集 matched_keys
# 仅用于 pop 和异常恢复,不再重复检测 C 扩展
matched_keys = [
key
for key in list(sys.modules.keys())
Expand Down
Loading