""" ultralytics 导入兼容层。 背景:本框架根目录下有一个同名的 `ultralytics/` 目录(YOLO 源码仓库根, 顶层无真正可 import 的包语义,或嵌套 `ultralytics/ultralytics/`)。 运行脚本通常把框架根插到 sys.path 首位(为 `import config` / `from modules import ...`), 这会让该目录以"命名空间包"形式遮蔽已(pip -e)安装、真正的 ultralytics 包, 导致 `from ultralytics import YOLO` 取到空包而报 ImportError。 另外:若进程 cwd 恰好是框架根,sys.path[0] 常为 ''(空串),旧逻辑 `if not p: return False` 会漏判遮蔽,必须把 '' 解析成真实 cwd。 解决:导入前,把 sys.path 中"含同名 ultralytics/ 子目录但没有 __init__.py"的遮蔽条目 临时移除;若 sys.modules 里已有半残的 ultralytics 命名空间包也一并清掉再 import; 导入后再把移除的条目放回 sys.path 末尾。 """ from __future__ import annotations import os import sys def _framework_root(): return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def _resolve_path_entry(p: str) -> str: """把 sys.path 条目解析成绝对目录。'' / '.' → cwd。""" if p is None: return "" if p == "" or p == ".": return os.path.abspath(os.getcwd()) return os.path.abspath(p) def _is_shadowing(p: str) -> bool: """该 path 项下是否有同名的、非真正包的 ultralytics/ 目录(命名空间遮蔽源)。""" root = _resolve_path_entry(p) if not root: return False d = os.path.join(root, "ultralytics") # 顶层 ultralytics/ 无 __init__.py → 命名空间包遮蔽 if os.path.isdir(d) and not os.path.exists(os.path.join(d, "__init__.py")): return True return False def import_yolo(): """返回 ultralytics.YOLO,规避框架根下同名目录的命名空间包遮蔽。""" # 1) 去掉遮蔽 path shadow = [p for p in list(sys.path) if _is_shadowing(p)] for p in shadow: try: sys.path.remove(p) except ValueError: pass # 2) 清掉可能已缓存的半残 ultralytics 模块(命名空间包 / 空包) doomed = [k for k in list(sys.modules) if k == "ultralytics" or k.startswith("ultralytics.")] # 仅当当前 ultralytics 看起来不可用(无 YOLO)时才清 need_clear = False mod = sys.modules.get("ultralytics") if mod is None: need_clear = False else: if not hasattr(mod, "YOLO"): need_clear = True else: # 即便有 YOLO,若 __file__ 指向框架根下命名空间也不可靠——有 YOLO 就信 need_clear = False if need_clear: for k in doomed: sys.modules.pop(k, None) try: from ultralytics import YOLO # noqa: WPS433 if not hasattr(YOLO, "__call__") and not callable(YOLO): # 极端兜底:再清一次强刷 for k in list(sys.modules): if k == "ultralytics" or k.startswith("ultralytics."): sys.modules.pop(k, None) from ultralytics import YOLO # noqa: WPS433 return YOLO finally: for p in shadow: if p not in sys.path: sys.path.append(p)