在GitHub Actions中使用mypy时如何忽略ModuleNotFoundError?
我正在将mypy集成到GitHub Actions工作流中,用于仅检查Django项目中变更的文件,当前工作流配置如下:
name: Pull Request Backend Lint and Format on: [push, pull_request] jobs: backend-lint-and-check: runs-on: ubuntu-latest defaults: run: working-directory: backend steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install ruff mypy mypy-extensions django-stubs django-stubs-ext - name: Get changed files id: changed-files run: | if [ "${{ github.event_name }}" == "pull_request" ]; then BASE_SHA=${{ github.event.pull_request.base.sha }} else BASE_SHA=$(git rev-parse HEAD^) fi CHANGED_FILES=$(git diff --name-only --diff-filter=d $BASE_SHA...HEAD | grep '^backend/' | sed 's|^backend/||' | grep '\.py$' | tr '\n' ' ') echo "files<<EOF" >> $GITHUB_OUTPUT echo "$CHANGED_FILES" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Run Ruff check if: steps.changed-files.outputs.files run: | FILES="${{ steps.changed-files.outputs.files }}" if [ ! -z "$FILES" ]; then echo "Running Ruff check on files:" echo "$FILES" for file in $FILES; do ruff check "$file" || echo "Ruff check found issues in $file" done else echo "No Python files to check" fi - name: Run Ruff format check if: steps.changed-files.outputs.files run: | FILES="${{ steps.changed-files.outputs.files }}" if [ ! -z "$FILES" ]; then echo "Running Ruff format check on files:" echo "$FILES" for file in $FILES; do ruff format --check -v "$file" || echo "Ruff format check found issues in $file" done else echo "No Python files to check" fi - name: Set up environment run: | if [ -f credentials/secrets.sh ]; then source credentials/secrets.sh fi if [ -f deployment/config-dev.sh ]; then source deployment/config-dev.sh fi - name: Run Mypy if: steps.changed-files.outputs.files run: | FILES="${{ steps.changed-files.outputs.files }}" if [ ! -z "$FILES" ]; then echo "Running Mypy on files:" echo "$FILES" for file in $FILES; do mypy --config-file setup.cfg --follow-imports=skip "$file" || echo "Mypy found issues in $file" done else echo "No Python files to check" fi
运行时遇到ModuleNotFoundError,报错信息如下:
Running Mypy on files: gh-actions-test.py Traceback (most recent call last): Error constructing plugin instance of NewSemanalDjangoPlugin File "/opt/hostedtoolcache/Python/3.12.5/x64/bin/mypy", line 8, in <module> sys.exit(console_entry()) ^^^^^^^^^^^^^^^ File "/opt/hostedtoolcache/Python/3.12.5/x64/lib/python3.12/site-packages/mypy/__main__.py", line 15, in console_entry main() File "mypy/main.py", line 103, in main File "mypy/main.py", line 187, in run_build File "mypy/build.py", line 193, in build File "mypy/build.py", line 238, in _build File "mypy/build.py", line 503, in load_plugins File "mypy/build.py", line 484, in load_plugins_from_config File "/opt/hostedtoolcache/Python/3.12.5/x64/lib/python3.12/site-packages/mypy_django_plugin/main.py", line 71, in __init__ self.django_context = DjangoContext(self.plugin_config.django_settings_module) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/hostedtoolcache/Python/3.12.5/x64/lib/python3.12/site-packages/mypy_django_plugin/django/context.py", line 127, in __init__ apps, settings = initialize_django(self.django_settings_module) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/hostedtoolcache/Python/3.12.5/x64/lib/python3.12/site-packages/mypy_django_plugin/django/context.py", line 81, in initialize_django settings._setup() # type: ignore[misc] ^^^^^^^^^^^^^^^^^ File "/opt/hostedtoolcache/Python/3.12.5/x64/lib/python3.12/site-packages/django/conf/__init__.py", line 68, in _setup self._wrapped = Settings(settings_module) ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/hostedtoolcache/Python/3.12.5/x64/lib/python3.12/site-packages/django/conf/__init__.py", line 166, in __init__ mod = importlib.import_module(self.SETTINGS_MODULE) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/opt/hostedtoolcache/Python/3.12.5/x64/lib/python3.12/importlib/__init__.py", line 90, in import_module return _bootstrap._gcd_import(name[level:], package, level) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<frozen importlib._bootstrap>", line 1387, in _gcd_import File "<frozen importlib._bootstrap>", line 1360, in _find_and_load File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked File "<frozen importlib._bootstrap>", line 935, in _load_unlocked File "<frozen importlib._bootstrap_external>", line 995, in exec_module File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed File "/home/runner/work/myproject/myproject/backend/myproject/settings.py", line 13, in <module> import openai ModuleNotFoundError: No module named 'openai' Mypy found issues in gh-actions-test.py
安装全量依赖(pip install -r requirements.txt)可解决问题,但会大幅拖慢工作流。以下是几种无需安装全量依赖即可跳过这类错误、获得有效类型检查的方法:
方法1:安装缺失依赖的类型Stub包
Mypy无需实际依赖包,只要有对应的类型定义包(通常命名为types-<package名>)就能完成类型检查。在GitHub Actions的依赖安装步骤中,添加需要的类型Stub包即可:
pip install ruff mypy mypy-extensions django-stubs django-stubs-ext types-openai
这种方式比安装全量依赖快很多,因为Stub包体积小、安装耗时短。
方法2:修改Django配置文件,避免强制导入第三方依赖
在项目的settings.py中,对第三方依赖做条件导入处理,避免Mypy初始化Django上下文时触发导入错误:
# settings.py try: import openai OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") except ImportError: openai = None OPENAI_API_KEY = ""
这样即使没有安装openai,Django配置文件也能正常加载,Mypy插件初始化不会失败。
方法3:使用Mock版Django配置文件
创建一个简化版的mypy_settings.py,仅包含Mypy类型检查必须的Django配置,移除所有第三方依赖相关的导入和配置:
# mypy_settings.py from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = "dummy-key-for-mypy" DEBUG = True INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", # 仅保留项目核心应用,移除依赖第三方包的应用 ] DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } }
然后在Mypy运行命令中指定使用这个Mock配置:
mypy --config-file setup.cfg --follow-imports=skip --django-settings-module=mypy_settings "$file"
方法4:调整Mypy插件配置,跳过Django上下文初始化
如果不需要Django特定的类型检查支持,可以禁用django-stubs插件,仅用基础Mypy检查:
在setup.cfg中注释或删除Django插件配置:
# [mypy] # plugins = mypy_django_plugin.main
这种方式会丢失Django相关的类型提示(比如模型、视图的类型检查),但能快速完成基础类型检查,适合对Django特定类型要求不高的场景。
内容的提问来源于stack exchange,提问作者Mustafa Marzouk

