Skip to content

fix: US module namespace collision — cores/ import resolution failure - #227

Merged
dragon1086 merged 3 commits into
dragon1086:mainfrom
lifrary:fix/us-namespace-collision-imports
Mar 29, 2026
Merged

dragon1086 merged 3 commits into
dragon1086:mainfrom
lifrary:fix/us-namespace-collision-imports

Conversation

@lifrary

@lifrary lifrary commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Summary

When running us_stock_analysis_orchestrator.py --mode morning, two ImportErrors occur because Python's module cache resolves from cores.xxx against the main project's cores/ instead of prism-us/cores/.

Root Cause

Early imports (e.g., TelegramConfig) cause Python to cache the main project's cores package. Once cached, all subsequent from cores.xxx import ... statements resolve against cores/ at the project root — even when the target module only exists in prism-us/cores/.

Errors

  1. us_stock_analysis_orchestrator.py:242

    ImportError: cannot import name 'prefetch_us_macro_intelligence_data' from 'cores.data_prefetch'
    

    → prefetch_us_macro_intelligence_data exists in prism-us/cores/data_prefetch.py, not in cores/data_prefetch.py

  2. us_trigger_batch.py:37

    ModuleNotFoundError: No module named 'cores.us_surge_detector'
    

    → us_surge_detector.py exists only in prism-us/cores/, not in main cores/

Fix

Use importlib.util.spec_from_file_location() to bypass Python's module cache and load directly from prism-us/cores/. This is the same pattern already used by _import_from_main_cores() for the reverse direction (loading main cores/ modules from within prism-us/).

  • us_stock_analysis_orchestrator.py: Added _import_from_us_cores() helper (counterpart to _import_from_main_cores()) and replaced the failing import at line 242
  • us_trigger_batch.py: Replaced from cores.us_surge_detector import ... with file-based import using importlib.util

Related

Test plan

  • python3 prism-us/us_stock_analysis_orchestrator.py --mode morning runs without ImportError
  • Macro intelligence prefetch completes successfully
  • Trigger batch (surge detection) completes successfully
  • Full analysis pipeline with detected stocks (requires active market triggers)

🤖 Generated with Claude Code

lifrary and others added 2 commits March 25, 2026 17:25
When running us_stock_analysis_orchestrator.py, Python caches the main
project's `cores` package first (triggered by early imports like
TelegramConfig). All subsequent `from cores.xxx` imports then resolve
against the main `cores/` directory, even when the target module only
exists in `prism-us/cores/`.

This caused two ImportErrors:
1. `from cores.data_prefetch import prefetch_us_macro_intelligence_data`
   → main cores/data_prefetch.py does not have this function
2. `from cores.us_surge_detector import ...`
   → main cores/ does not have us_surge_detector.py at all

Fix: use file-based imports (importlib.util.spec_from_file_location) to
bypass Python's module cache and load directly from prism-us/cores/.
This is the same pattern already used by `_import_from_main_cores()` for
the reverse direction.

Related: dragon1086#220 (same class of bug in us_pending_order_batch.py)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Same class of bug as the previous commit — `from cores.xxx` resolves
against main cores/ instead of prism-us/cores/:

1. `create_us_macro_intelligence_agent` (line 270): only exists in
   prism-us/cores/agents/macro_intelligence_agent.py
2. `analyze_us_stock` (line 459): only exists in
   prism-us/cores/us_analysis.py

Both now use `_import_from_us_cores()` for file-based import.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@dragon1086

dragon1086 commented Mar 26, 2026 •

Copy link
Copy Markdown
Owner

Hey, thanks for the PR!

One thing I wanted to check — we've been running the exact same setup on our production server:

cd /root/prism-insight && python prism-us/us_stock_analysis_orchestrator.py --mode morning

and we haven't seen this ImportError come up at all.

Looking at the code, the sys.path setup does this:

sys.path.insert(0, str(PROJECT_ROOT))   # project root
sys.path.insert(0, str(PRISM_US_DIR))   # prism-us/ (ends up at index 0)

Since prism-us/ lands at sys.path[0], from cores.xxx should resolve to prism-us/cores/ first. So in theory, the namespace collision described in the PR shouldn't happen under normal execution.

Could you share a bit more about the environment where you hit this? (Python version, exact command you ran, or whether it was from an IDE, etc.)

Also a minor concern with the importlib.util approach — it could introduce some subtle side effects:

  • The loaded modules don't get registered in sys.modules, so the same module could end up loaded twice with separate state
  • Any internal from cores.xxx import ... inside those modules still goes through sys.path, which could cause inconsistencies
  • Makes it harder to trace which path a module was loaded from when debugging

If you can share the repro steps, happy to figure out the right fix together!


(혹시 한국분이시면 편하게 한국어로 답변 주셔도 됩니다!)

@lifrary

lifrary commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor Author

안녕하세요! 한국어 답변 감사합니다 😄 한국어로 쓸게요.

재현 환경

  • macOS (Apple Silicon), Python 3.14.0a5
  • 실행 명령: PRISM_OPENAI_AUTH_MODE=chatgpt_oauth python3 prism-us/us_stock_analysis_orchestrator.py --mode morning
  • ChatGPT OAuth 프록시 모드에서만 발생합니다.

Root Cause

리뷰어분 말씀이 맞습니다. sys.path 순서상으로는 PRISM_US_DIR이 먼저이므로 정상적으로는 from cores.xxx가 prism-us/cores/를 먼저 찾습니다.

문제는 main() 함수의 chatgpt_oauth 분기 (line 1189-1192)입니다:

if not args.no_proxy and os.getenv("PRISM_OPENAI_AUTH_MODE") == "chatgpt_oauth":
    sys.path.insert(0, str(Path(__file__).resolve().parent.parent))  # PROJECT_ROOT
    from cores.chatgpt_proxy import inject_env, start_proxy          # ← 여기

이 from cores.chatgpt_proxy import ...가 실행되면:

  1. Python이 cores 패키지를 PROJECT_ROOT/cores/ 에서 찾아 로드
  2. sys.modules['cores']에 메인 프로젝트의 cores/가 캐싱됨
  3. 이후 run_macro_intelligence() 등에서 from cores.data_prefetch import ... 실행 시, sys.modules 캐시 때문에 sys.path 순서와 무관하게 메인 cores/data_prefetch.py에서 찾음
  4. prefetch_us_macro_intelligence_data는 prism-us/cores/data_prefetch.py에만 있으므로 ImportError

production에서 안 나는 이유는 API 키 모드를 사용하면 line 1189 조건이 false가 되어 cores.chatgpt_proxy import 자체가 실행되지 않고, sys.modules 캐싱이 일어나지 않기 때문입니다.

더 나은 수정 방향

리뷰어분의 importlib.util side-effect 우려(이중 로딩, 내부 import 불일치, 디버깅 어려움)에 동의합니다.

현재 PR은 _import_from_us_cores()를 4곳에 적용했는데, root cause를 직접 수정하는 게 더 깔끔할 것 같습니다. chatgpt_proxy import 3곳만 _import_from_main_cores()로 변경하면 sys.modules 오염 자체가 방지됩니다:

# Before (sys.modules['cores']를 메인으로 캐싱)
from cores.chatgpt_proxy import inject_env, start_proxy

# After (importlib.util로 직접 로드, sys.modules 오염 없음)
_proxy_mod = _import_from_main_cores("cores_chatgpt_proxy", "cores/chatgpt_proxy/__init__.py")
inject_env = _proxy_mod.inject_env
start_proxy = _proxy_mod.start_proxy

이렇게 하면:

  • cores 패키지가 sys.modules에 캐싱되지 않음
  • 이후 from cores.xxx가 sys.path 순서대로 prism-us/cores/에서 정상 해석
  • _import_from_us_cores() 헬퍼와 3곳의 수정이 불필요해짐
  • importlib.util 사용 범위가 오히려 줄어듦

원하시면 이 방향으로 PR을 업데이트하겠습니다!

@dragon1086
dragon1086 self-requested a review March 26, 2026 15:38
@dragon1086 dragon1086 added bug Something isn't working good first issue Good for newcomers labels Mar 26, 2026
@dragon1086

dragon1086 commented Mar 26, 2026 •

Copy link
Copy Markdown
Owner

와 분석 정말 깔끔하시네요! 👍 root cause 정확히 짚어주셨습니다.

chatgpt_oauth 모드에서만 발생한다는 점, sys.modules['cores'] 캐싱이 원인이라는 점, 그리고 운영서버에서 안 나는 이유(API 키 모드라 해당 분기 자체를 안 탄다)까지, 제가 놓치고 있던 부분이었어요. 감사합니다.

제안해주신 _import_from_main_cores()로 chatgpt_proxy 3곳만 바꾸는 방향에 저도 동의합니다. 기존 _import_from_us_cores() 4곳 수정보다 훨씬 깔끔하고요.

다만 제가 코드를 좀 더 살펴보다가 보완이 필요한 부분 3가지를 발견해서 공유드려요. 이 부분까지 반영하시면 훨씬 견고한 수정이 될 것 같습니다.


보완 사항 3가지

1. chatgpt_proxy는 패키지라서 _import_from_main_cores가 바로 안 됩니다

cores/chatgpt_proxy/__init__.py 내부에 상대 import가 있어요:

from .constants import DEFAULT_PROXY_PORT
from .proxy_server import create_app
from .token_manager import TokenManager

importlib.util.spec_from_file_location으로 로드하면 __package__가 설정이 안 되기 때문에 상대 import에서 바로 에러납니다:

ImportError: attempted relative import with no known parent package

해결 방법: spec_from_file_location 호출 시 submodule_search_locations 지정 + __package__ 수동 설정이 필요합니다. 아래 "구현 가이드" 섹션에 예시 넣어뒀어요.

2. 모듈 인스턴스 싱글톤 문제

chatgpt_proxy/__init__.py에 모듈 레벨 상태가 있습니다:

_runner: web.AppRunner | None = None
_site: web.TCPSite | None = None

_import_from_main_cores는 sys.modules에 등록을 안 하기 때문에, 3곳(line 1174, 1179, 1199)에서 각각 호출하면 별도 모듈 인스턴스 3개가 만들어집니다.

그러면:

  • start_proxy()가 인스턴스 A에서 _runner 세팅
  • stop_proxy()가 인스턴스 C에서 _runner 읽음 → None → 프록시 안 꺼짐 → 포트 누수

한번만 로드해서 캐싱하는 방식으로 해야 합니다.

3. line 1173, 1198의 sys.path.insert(0, PROJECT_ROOT) 제거 필요

_import_from_main_cores로 바꾸면 sys.path를 건드릴 이유가 없어지는데, 이 줄들이 남아있으면 sys.path 순서가 바뀌어서 이후 from cores.* import에 영향을 줄 수 있어요. 같이 제거하시면 됩니다.


구현 가이드 (AI 도구나 직접 작업 시 참고하시라고 상세하게 적어봤습니다)

PRD (요구사항)

항목 내용
목표 chatgpt_oauth 모드에서 sys.modules['cores'] 오염으로 인한 ImportError 해결
범위 prism-us/us_stock_analysis_orchestrator.py 단일 파일 수정
비범위 us_trigger_batch.py 등 다른 파일의 _import_from_us_cores 적용 (불필요)
제약 기존 API 키 모드 동작에 영향 없어야 함

수정 계획

Step 1: _import_from_main_cores 헬퍼를 패키지 로딩 지원하도록 확장 (또는 별도 헬퍼 추가)

def _import_package_from_main_cores(package_name: str, relative_dir: str):
    """
    Import a package (with relative imports) from main project cores/.
    Unlike _import_from_main_cores, this handles __init__.py with relative imports.
    """
    import importlib.util, types
    pkg_dir = PROJECT_ROOT / relative_dir

    # cores 패키지가 sys.modules에 없으면 synthetic parent 등록
    if "cores" not in sys.modules:
        cores_pkg = types.ModuleType("cores")
        cores_pkg.__path__ = [str(PROJECT_ROOT / "cores")]
        cores_pkg.__package__ = "cores"
        sys.modules["cores"] = cores_pkg

    spec = importlib.util.spec_from_file_location(
        package_name,
        pkg_dir / "__init__.py",
        submodule_search_locations=[str(pkg_dir)]
    )
    module = importlib.util.module_from_spec(spec)
    module.__package__ = package_name
    sys.modules[package_name] = module
    spec.loader.exec_module(module)
    return module

⚠️ 주의: 이 방식은 sys.modules["cores"]에 synthetic 모듈을 넣으므로, __path__가 PROJECT_ROOT/cores/를 가리킵니다. 이러면 이후 from cores.data_prefetch가 메인 cores에서 해석될 수 있어요.

그래서 더 안전한 대안은 아래처럼 save/restore 패턴입니다:

def _import_proxy_safe():
    """chatgpt_proxy를 로드하되 sys.modules['cores'] 오염 방지"""
    saved = sys.modules.get("cores")
    saved_path = list(sys.path)
    try:
        sys.path.insert(0, str(PROJECT_ROOT))
        # 캐시된 cores가 있으면 제거해서 fresh resolve
        if "cores" in sys.modules:
            del sys.modules["cores"]
        import cores.chatgpt_proxy as proxy_mod
        return proxy_mod
    finally:
        # 원래 상태 복원
        sys.path[:] = saved_path
        if saved is not None:
            sys.modules["cores"] = saved
        elif "cores" in sys.modules:
            del sys.modules["cores"]

Step 2: main() 함수의 chatgpt_oauth 블록 수정

# Before (현재 코드 - 3곳에서 from cores.chatgpt_proxy import ...)
if not args.no_proxy and os.getenv("PRISM_OPENAI_AUTH_MODE") == "chatgpt_oauth":
    try:
        sys.path.insert(0, str(Path(__file__).resolve().parent.parent))  # ← 제거
        from cores.chatgpt_proxy import inject_env, start_proxy          # ← 변경
        ...

# After
if not args.no_proxy and os.getenv("PRISM_OPENAI_AUTH_MODE") == "chatgpt_oauth":
    try:
        _proxy_mod = _import_proxy_safe()   # 한 번만 로드
        inject_env = _proxy_mod.inject_env
        start_proxy = _proxy_mod.start_proxy
        clear_env = _proxy_mod.clear_env
        stop_proxy = _proxy_mod.stop_proxy

        inject_env()
        proxy_started = await start_proxy()
        if not proxy_started:
            logger.warning("ChatGPT OAuth proxy failed to start, falling back to standard API")
            clear_env()
    except Exception as e:
        logger.warning("ChatGPT OAuth proxy setup error: %s, falling back to standard API", e)

그리고 line 1196-1202의 stop_proxy 블록도:

# Before
if proxy_started:
    try:
        sys.path.insert(0, str(Path(__file__).resolve().parent.parent))  # ← 제거
        from cores.chatgpt_proxy import stop_proxy                       # ← 제거
        await stop_proxy()

# After (위에서 이미 stop_proxy를 꺼내뒀으므로)
if proxy_started:
    try:
        await stop_proxy()  # 위에서 캐싱한 같은 모듈 인스턴스
    except Exception:
        pass

Step 3: 원래 PR에서 추가한 _import_from_us_cores() 헬퍼와 관련 4곳 변경 → 전부 되돌리기 (불필요)

변경 요약

파일 변경 내용
us_stock_analysis_orchestrator.py _import_proxy_safe() 헬퍼 추가 (약 15줄)
us_stock_analysis_orchestrator.py line 1173 sys.path.insert 제거
us_stock_analysis_orchestrator.py line 1174, 1179 → _proxy_mod에서 추출
us_stock_analysis_orchestrator.py line 1198-1199 sys.path.insert + import 제거
us_stock_analysis_orchestrator.py _import_from_us_cores 헬퍼 + 4곳 수정 되돌리기
us_trigger_batch.py 원래 PR 변경 되돌리기 (수정 불필요)

E2E 테스트 방법

수정 후 아래 순서로 검증하시면 됩니다.

테스트 1: chatgpt_oauth 모드 (버그 재현 → 수정 확인)

# 수정 전 (현재 PR 상태) — ImportError 재현되는지 확인
PRISM_OPENAI_AUTH_MODE=chatgpt_oauth python3 prism-us/us_stock_analysis_orchestrator.py --mode morning

# 수정 후 — ImportError 없이 정상 진행되는지 확인
PRISM_OPENAI_AUTH_MODE=chatgpt_oauth python3 prism-us/us_stock_analysis_orchestrator.py --mode morning

OAuth 토큰이 만료된 상태라면 프록시 시작이 실패하겠지만, 핵심은 ImportError가 안 나고 graceful fallback 되는지입니다. 로그에 아래처럼 나오면 성공:

WARNING - ChatGPT OAuth proxy failed to start, falling back to standard API

(이후 일반 API 키로 분석이 계속 진행되면 OK)

테스트 2: 일반 API 키 모드 (기존 동작 regression 없는지)

# chatgpt_oauth 없이 실행 — 기존대로 잘 돌아야 함
python3 prism-us/us_stock_analysis_orchestrator.py --mode morning

테스트 3: sys.modules 오염 확인 (디버깅용)

import 직후에 간단한 확인 코드를 임시로 넣어서 체크할 수 있어요:

# _import_proxy_safe() 호출 후에 임시로 추가
print(f"cores in sys.modules: {'cores' in sys.modules}")
print(f"sys.path[0]: {sys.path[0]}")
# cores가 sys.modules에 없거나, 있더라도 prism-us/cores/ 를 가리키면 OK
if "cores" in sys.modules:
    print(f"cores.__path__: {sys.modules['cores'].__path__}")

테스트 4: 프록시 종료 확인 (포트 누수 체크)

# 실행 전
lsof -i :18741  # 아무것도 없어야 함

# 실행 후 (프로세스 종료 후)
lsof -i :18741  # 역시 아무것도 없어야 함 — 포트 정리 확인

검증 체크리스트

  • chatgpt_oauth 모드: ImportError 없이 실행 (또는 graceful fallback)
  • 일반 API 키 모드: regression 없음
  • sys.modules['cores']가 prism-us/cores/를 가리키거나 미등록 상태
  • sys.path 순서가 원래대로 유지 (PRISM_US_DIR이 PROJECT_ROOT보다 앞)
  • 프록시 시작/종료 시 포트 18741 정상 해제
  • us_trigger_batch.py 수정 없이 정상 동작

수정하시면서 궁금한 점 있으시면 편하게 물어봐주세요!

Address maintainer review feedback on PR dragon1086#227:

1. chatgpt_proxy is a package with relative imports, so
   importlib.util.spec_from_file_location fails. Use standard import
   with sys.modules/sys.path save/restore instead.

2. Load proxy module once to avoid singleton issues — module-level
   state (_runner, _site) must be shared across start/stop calls.

3. Remove unnecessary sys.path.insert(0, PROJECT_ROOT) lines.

4. Revert _import_from_us_cores helper and all 3 call sites — root
   cause fix (preventing sys.modules pollution) makes them unnecessary.

5. Revert us_trigger_batch.py changes — not affected by this bug.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@lifrary

lifrary commented Mar 27, 2026 •

Copy link
Copy Markdown
Contributor Author

피드백 감사합니다! 세 가지 보완 사항 모두 반영해서 업데이트했습니다.

변경 내용

_import_from_us_cores() 헬퍼와 관련 4곳 수정을 전부 제거하고, 제안해주신 save/restore 패턴으로 교체했습니다.

def _import_proxy_safe():
    saved_cores = sys.modules.get("cores")
    saved_path = list(sys.path)
    try:
        sys.path.insert(0, str(PROJECT_ROOT))
        if "cores" in sys.modules:
            del sys.modules["cores"]
        import cores.chatgpt_proxy as proxy_mod
        return proxy_mod
    finally:
        sys.path[:] = saved_path
        for key in [k for k in sys.modules if k.startswith("cores.chatgpt_proxy")]:
            del sys.modules[key]
        if saved_cores is not None:
            sys.modules["cores"] = saved_cores
        elif "cores" in sys.modules:
            del sys.modules["cores"]

반영 사항:

  1. 표준 import 사용 → 상대 import 문제 없음
  2. 한 번만 로드 후 inject_env, start_proxy, clear_env, stop_proxy 추출 → 싱글톤 문제 없음
  3. sys.path.insert(0, PROJECT_ROOT) 2곳 제거
  4. us_trigger_batch.py 변경 되돌림 (수정 불필요)

추가 반영:

  • cores.chatgpt_proxy.* 서브모듈 키도 sys.modules에서 정리 (서브모듈 확인 결과 런타임 lazy import 없어서 안전)
  • except 블록에서 clear_env() 호출 추가 (inject_env() 후 예외 발생 시 환경변수 정리)

검증 결과:

  • _import_proxy_safe() 정상 로드 + sys.modules 정리 확인
  • chatgpt_oauth 모드: ImportError 없이 proxy 시작, macro prefetch 정상 진행
  • from cores.data_prefetch, macro_intelligence_agent, us_analysis → 모두 prism-us/cores/에서 해석
  • sys.path 순서 유지 (PRISM_US_DIR first)
  • us_trigger_batch.py 원래 import 정상 동작
  • 프록시 종료 후 포트 18741 정상 해제

Summary for English readers:

All 3 issues have been addressed in this update.

Issue 1 — Relative import failure: chatgpt_proxy is a package with from .constants import ... style imports, so importlib.util.spec_from_file_location would fail with ImportError: attempted relative import with no known parent package. Fixed by switching to a save/restore pattern that uses standard import, which handles relative imports natively.

Issue 2 — Module singleton problem: The proxy module has module-level state (_runner, _site) that must be shared between start_proxy() and stop_proxy(). Previously, separate imports would create independent module instances, causing port leaks. Fixed by loading the module once via _import_proxy_safe() and extracting all 4 functions from the single instance.

Issue 3 — Leftover sys.path.insert: Both sys.path.insert(0, PROJECT_ROOT) calls (lines 1173 and 1198) have been removed since _import_proxy_safe() handles path manipulation internally and restores it afterward.

Additional changes:

  • Reverted _import_from_us_cores() helper and all 3 call sites back to standard from cores.xxx import ... — no longer needed once root cause is fixed
  • Reverted us_trigger_batch.py entirely — not affected by this bug
  • Cleaned up cores.chatgpt_proxy.* sub-module keys from sys.modules in the finally block (verified all sub-modules use top-level imports only, no runtime lazy imports)
  • Added clear_env() call in the except path to clean up env vars if inject_env() succeeded but a later step fails

Verification (macOS, Python 3.14):

  • _import_proxy_safe() loads correctly + sys.modules cleanup confirmed
  • chatgpt_oauth mode: no ImportError, proxy started on port 18741, macro prefetch runs normally
  • from cores.data_prefetch, macro_intelligence_agent, us_analysis — all resolved from prism-us/cores/
  • sys.path order preserved (PRISM_US_DIR first)
  • us_trigger_batch.py works with original imports
  • Port 18741 released after proxy shutdown

@dragon1086 dragon1086 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@lifrary 로컬 E2E테스트 완료했습니다. 최초 기여에 진심으로 감사드립니다. 운영서버에 반영하여 테스트 후 이상 생기면 제가 자체적으로 수정하겠습니다. 추후 릴리즈노트에 해당 PR언급하도록 하겠습니다. 감사합니다.

@dragon1086
dragon1086 merged commit a67d2a4 into dragon1086:main Mar 29, 2026
choco52972-code pushed a commit to choco52972-code/prism-insight that referenced this pull request May 21, 2026
Address maintainer review feedback on PR dragon1086#227:

1. chatgpt_proxy is a package with relative imports, so
   importlib.util.spec_from_file_location fails. Use standard import
   with sys.modules/sys.path save/restore instead.

2. Load proxy module once to avoid singleton issues — module-level
   state (_runner, _site) must be shared across start/stop calls.

3. Remove unnecessary sys.path.insert(0, PROJECT_ROOT) lines.

4. Revert _import_from_us_cores helper and all 3 call sites — root
   cause fix (preventing sys.modules pollution) makes them unnecessary.

5. Revert us_trigger_batch.py changes — not affected by this bug.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
choco52972-code pushed a commit to choco52972-code/prism-insight that referenced this pull request May 21, 2026
…sion-imports

fix: US module namespace collision — cores/ import resolution failure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working good first issue Good for newcomers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants