Search code examples
djangopytestgithub-actionsdotenv

pytest fail on github action. TypeError: expected str, bytes or os.PathLike object, not NoneType


When I run pytest locally, it works well. But it fails on GitHub Actions.

I set a secret key on GitHub secrets and uploaded .env.test to the GitHub repository both.

Is it a dotenv package problem or something else?

workflow.yml

name: Django CI

on: [pull_request, push] # activates the workflow when there is a push or pull request in the repo

env:
  SERVER_ENV: "test"
  DEBUG: ${{secrets.SECRET_KEY}}
  SECRET_KEY: ${{secrets.SECRET_KEY}}
  DB_ENGINE: ${{secrets.DB_ENGINE}}
  DB_NAME: ${{secrets.DB_NAME}}
  USER: ${{secrets.USER}}
  PASSWORD: ${{secrets.PASSWORD}}
  POSTGRES_USER: ${{secrets.POSTGRES_USER}}
  POSTGRES_PASSWORD: ${{secrets.POSTGRES_PASSWORD}}
  POSTGRES_DB: ${{secrets.POSTGRES_DB}}
  HOST: ${{secrets.HOST}}
  PORT: ${{secrets.PORT}}
  CELERY_BROKER_URL: ${{secrets.CELERY_BROKER_URL}}
  LOG_LEVEL: ${{secrets.LOG_LEVEL}}
  NAVER_NEWS_API_CLIENT_ID: ${{secrets.NAVER_NEWS_API_CLIENT_ID}}
  NAVER_NEWS_API_CLIENT_SECRET: ${{secrets.NAVER_NEWS_API_CLIENT_SECRET}}
  DJANGO_ALLOWED_HOSTS: ${{secrets.DJANGO_ALLOWED_HOSTS}}

jobs:
  test_project:
    runs-on: ubuntu-latest # operating system your code will run on
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
        with:
          python-version: 3.8
      - name: Install Dependencies
        run: |
          pip install --upgrade pip
          python3.8 -m venv env
          source env/bin/activate
          pip install -r requirements.txt # install all our dependencies for the project
      - name: Run Pytest
        run: pytest . # run pytest test

I tried like this workflow.yml

jobs:
  test_project:
    runs-on: ubuntu-latest # operating system your code will run on
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
        with:
          python-version: 3.8
      - name: Create venv
        run: |
          pip install --upgrade pip
          python3.8 -m venv venv
          source venv/bin/activate
      - name: check python version
        run: python -V
      - name: check python version
        run: venv/bin/python -V
      - name: Install Dependencies
        run: venv/bin/python -m pip install -r requirements.txt # install all our dependencies for the project
      - name: Run Pytest
        run: venv/bin/python -m pytest

Error log:

Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.11.1/x64/bin/pytest", line 8, in <module>
    sys.exit(console_main())
             ^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/_pytest/config/__init__.py", line 190, in console_main
    code = main()
           ^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/_pytest/config/__init__.py", line 148, in main
    config = _prepareconfig(args, plugins)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/_pytest/config/__init__.py", line 329, in _prepareconfig
    config = pluginmanager.hook.pytest_cmdline_parse(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_hooks.py", line 265, in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_manager.py", line 80, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_callers.py", line 55, in _multicall
    gen.send(outcome)
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/_pytest/helpconfig.py", line 103, in pytest_cmdline_parse
    config: Config = outcome.get_result()
                     ^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_result.py", line 60, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_callers.py", line 39, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/_pytest/config/__init__.py", line 1058, in pytest_cmdline_parse
    self.parse(args)
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/_pytest/config/__init__.py", line 1346, in parse
    self._preparse(args, addopts=addopts)
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/_pytest/config/__init__.py", line 1248, in _preparse
    self.hook.pytest_load_initial_conftests(
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_hooks.py", line 265, in __call__
    return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_manager.py", line 80, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_callers.py", line 60, in _multicall
    return outcome.get_result()
           ^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_result.py", line 60, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pluggy/_callers.py", line 39, in _multicall
    res = hook_impl.function(*args)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hostedtoolcache/Python/3.11.1/x64/lib/python3.11/site-packages/pytest_django_dotenv/plugin.py", line 17, in pytest_load_initial_conftests
    dotenv.read_dotenv(os.path.join(virtual_env_path, f'../{early_config.getini("env_path")[0]}'))
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen posixpath>", line 76, in join
TypeError: expected str, bytes or os.PathLike object, not NoneType
Error: Process completed with exit code 1.

requirements.txt

amqp==5.1.1
asgiref==3.6.0
async-timeout==4.0.2
attrs==22.2.0
backports.zoneinfo==0.2.1;python_version<"3.9"
beautifulsoup4==4.11.1
billiard==3.6.4.0
black==22.12.0
celery==5.2.7
certifi==2022.12.7
charset-normalizer==2.1.1
click==8.1.3
click-didyoumean==0.3.0
click-plugins==1.1.1
click-repl==0.2.0
Django==4.0.8
django-celery-beat==2.4.0
django-dotenv==1.4.2
django-environ==0.9.0
django-timezone-field==5.0
djangorestframework==3.14.0
djangorestframework-simplejwt==5.2.2
exceptiongroup==1.1.0
execnet==1.9.0
flower==1.2.0
gunicorn==20.1.0
humanize==4.4.0
idna==3.4
iniconfig==1.1.1
kombu==5.2.4
mypy-extensions==0.4.3
packaging==22.0
pathspec==0.10.3
platformdirs==2.6.0
pluggy==1.0.0
prometheus-client==0.15.0
prompt-toolkit==3.0.36
psycopg2-binary==2.9.5
PyJWT==2.6.0
pytest==7.2.0
pytest-django==4.5.2
pytest-django-dotenv==0.1.2
pytest-xdist==3.1.0
python-crontab==2.7.1
python-dateutil==2.8.2
pytz==2022.7
redis==4.4.0
requests==2.28.1
six==1.16.0
soupsieve==2.3.2.post1
sqlparse==0.4.3
tomli==2.0.1
tornado==6.2
typing_extensions==4.4.0
tzdata==2022.7
urllib3==1.26.13
vine==5.0.0
wcwidth==0.2.5

pytest.ini

[pytest]
DJANGO_SETTINGS_MODULE = modu_property.settings
python_files = test*.py
django_find_project = true
env_path = ./.env.test
log_cli = 1
log_cli_level = DEBUG
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format=%Y-%m-%d %H:%M:%S

settings.py

import datetime
import json
from pathlib import Path

import environ
import os

from celery.schedules import crontab

env = environ.Env(
    # set casting, default value
    # DEBUG=(bool, False)
)

# Set the project base directory
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


def set_logging():
    return {
        "version": 1,
        "disable_existing_loggers": False,
        "filters": {
            "require_debug_false": {
                "()": "django.utils.log.RequireDebugFalse",
            },
            "require_debug_true": {
                "()": "django.utils.log.RequireDebugTrue",
            },
        },
        "formatters": {
            "django.server": {
                "()": "django.utils.log.ServerFormatter",
                "format": "{asctime} {filename}:{funcName}:{lineno} [{levelname}] {message}",
                "style": "{",
            },
        },
        "handlers": {
            "django.server": {
                "level": env("LOG_LEVEL"),
                "class": "logging.StreamHandler",
                "formatter": "django.server",
            },
            "file": {
                "level": env("LOG_LEVEL"),
                "filters": ["require_debug_true"],
                "class": "logging.handlers.RotatingFileHandler",
                "filename": f"{BASE_DIR}/modu_property.log",
                "maxBytes": 1024 * 1024 * 5,  # 5 MB
                "backupCount": 5,
                "formatter": "django.server",
            },
        },
        "loggers": {
            "django.server": {
                "handlers": ["django.server"],
                "level": env("LOG_LEVEL"),
                "propagete": True,
            },
            "file": {
                "handlers": ["file"],
                "level": env("LOG_LEVEL"),
                "propagete": True,
            },
        },
    }


# FROM .env.* file
SERVER_ENV = os.environ.get("SERVER_ENV")
if SERVER_ENV == "dev":
    environ.Env.read_env(os.path.join(BASE_DIR, ".env.dev"))
elif SERVER_ENV == "stage":
    environ.Env.read_env(os.path.join(BASE_DIR, ".env.stage"))
# elif SERVER_ENV == "prod":
#     environ.Env.read_env(os.path.join(BASE_DIR, ".env.prod"))
elif SERVER_ENV == "test":
    environ.Env.read_env(os.path.join(BASE_DIR, ".env.test"))
else:
    environ.Env.read_env(os.path.join(BASE_DIR, ".env.local"))
LOGGING = set_logging()


# False if not in os.environ because of casting above
DEBUG = env("DEBUG")

# Raises Django's ImproperlyConfigured
# exception if SECRET_KEY not in os.environ
SECRET_KEY = env("SECRET_KEY")

CELERY_BROKER_URL = env("CELERY_BROKER_URL")


ENGINE = env("DB_ENGINE")
NAME = env("DB_NAME")
USER = env("USER")
PASSWORD = env("PASSWORD")
HOST = env("HOST")
PORT = env("PORT")
NAVER_NEW_API_CLIENT_ID = env("NAVER_NEW_API_CLIENT_ID")
NAVER_NEW_API_CLIENT_SECRET = env("NAVER_NEW_API_CLIENT_SECRET")
DJANGO_ALLOWED_HOSTS = env("DJANGO_ALLOWED_HOSTS", default="").split(" ")
ALLOWED_HOSTS = DJANGO_ALLOWED_HOSTS
POSTGRES_USER = env("POSTGRES_USER", default="")
POSTGRES_PASSWORD = env("POSTGRES_PASSWORD", default="")
POSTGRES_DB = env("POSTGRES_DB", default="")

# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "app",
    "accounts",
    "modu_property",
    "django_celery_beat",
    # "rest_framework_simplejwt",
]

# TODO : rest_framework_simplejwt 설정 필요 없으면 제거
# REST_FRAMEWORK = {
#     "DEFAULT_AUTHENTICATION_CLASSES": (
#         "rest_framework_simplejwt.authentication.JWTAuthentication",
#     )
# }

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    # "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "modu_property.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "modu_property.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases

DATABASES = {
    "default": {
        "ENGINE": ENGINE,
        "NAME": NAME,
        "USER": USER,
        "PASSWORD": PASSWORD,
        "HOST": HOST,
        "PORT": PORT,
    },
}


# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
    },
    {
        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
    },
]


# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": datetime.timedelta(hours=6),
    "REFRESH_TOKEN_LIFETIME": datetime.timedelta(days=14),
    "SIGNING_KEY": SECRET_KEY,
    "ALGORITHM": "HS256",
    "AUTH_HEADER_TYPES": ("JWT",),
}

AUTH_USER_MODEL = "accounts.User"

# Celery Configuration Options
CELERY_TIMEZONE = "UTC"
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60
CELERY_BROKER_URL = CELERY_BROKER_URL
CELERY_TASK_SERIALIZER = "json"
CELERY_RESULT_SERIALIZER = "json"
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_BEAT_SCHEDULE = {
    "collect_property_news_every_5_minutes": {
        "task": "modu_property.tasks.collect_property_news_task",
        "schedule": crontab(minute="*/5"),
        "args": json.dumps(
            {
                "days": 100,
            }
        ),
    },
}

enter image description here

venv local interpreter enter image description here


Solution

  • I ran pytest after activating venv and the build was successful:

    jobs:
      test_project:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v2
    
          - uses: actions/setup-python@v2
            with:
              python-version: 3.8
    
          - name: Install Dependencies
            run: |
              pip install --upgrade pip
              python3.8 -m venv env
              source env/bin/activate
              pip install -r requirements.txt
              pytest .