Skip to content

Commit

Permalink
fix #596 django override (#645)
Browse files Browse the repository at this point in the history
* Fix #596 django.test.override issue

* Fix CI side effects
  • Loading branch information
rochacbruno committed Aug 20, 2021
1 parent cf07219 commit d3b7575
Show file tree
Hide file tree
Showing 23 changed files with 396 additions and 25 deletions.
6 changes: 6 additions & 0 deletions Makefile
Expand Up @@ -143,6 +143,12 @@ test_redis:
watch:
ls **/**.py | entr py.test -m "not integration" -s -vvv -l --tb=long --maxfail=1 tests/

watch_django:
ls {**/**.py,~/.virtualenvs/dynaconf/**/**.py,.venv/**/**.py} | PYTHON_PATH=. DJANGO_SETTINGS_MODULE=foo.settings entr example/django_example/manage.py test polls -v 2

watch_coverage:
ls {**/**.py,~/.virtualenvs/dynaconf/**/**.py} | entr -s "make test;coverage html"

test_only:
py.test -m "not integration" -v --cov-config .coveragerc --cov=dynaconf -l --tb=short --maxfail=1 tests/
coverage xml
Expand Down
35 changes: 28 additions & 7 deletions dynaconf/base.py
@@ -1,3 +1,4 @@
import copy
import glob
import importlib
import inspect
Expand Down Expand Up @@ -91,10 +92,11 @@ class LazySettings(LazyObject):
and all values in a hash called DYNACONF_PROJ in redis
"""

def __init__(self, **kwargs):
def __init__(self, wrapped=None, **kwargs):
"""
handle initialization for the customization cases
:param wrapped: a deepcopy of this object will be wrapped (issue #596)
:param kwargs: values that overrides default_settings
"""

Expand All @@ -107,6 +109,13 @@ def __init__(self, **kwargs):
self._kwargs = kwargs
super(LazySettings, self).__init__()

if wrapped:
if self._django_override:
# This fixes django issue #596
self._wrapped = copy.deepcopy(wrapped)
else:
self._wrapped = wrapped

def __resolve_config_aliases(self, kwargs):
"""takes aliases for _FOR_DYNACONF configurations
Expand Down Expand Up @@ -225,6 +234,7 @@ class Settings:
"""

dynaconf_banner = BANNER
_store = DynaBox()

def __init__(self, settings_module=None, **kwargs): # pragma: no cover
"""Execute loaders and custom initialization
Expand Down Expand Up @@ -957,11 +967,9 @@ def loaders(self): # pragma: no cover
return []

if not self._loaders:
for loader_module_name in self.LOADERS_FOR_DYNACONF:
loader = importlib.import_module(loader_module_name)
self._loaders.append(loader)
self._loaders = self.LOADERS_FOR_DYNACONF

return self._loaders
return [importlib.import_module(loader) for loader in self._loaders]

def reload(self, env=None, silent=None): # pragma: no cover
"""Clean end Execute all loaders"""
Expand Down Expand Up @@ -1160,14 +1168,27 @@ def populate_obj(self, obj, keys=None, ignore=None):
if value is not empty:
setattr(obj, key, value)

def dynaconf(self): # pragma: no cover
def dynaconf_clone(self):
"""Clone the current settings object."""
return copy.deepcopy(self)

@property
def dynaconf(self):
"""A proxy to access internal methods and attributes
Starting in 3.0.0 Dynaconf now allows first level lower case
keys that are not reserved keyword, so this is a proxy to
internal methods and attrs.
"""
return # TOBE IMPLEMENTED

class AttrProxy(object):
def __init__(self, obj):
self.obj = obj

def __getattr__(self, name):
return getattr(self.obj, f"dynaconf_{name}")

return AttrProxy(self)

@property
def logger(self): # pragma: no cover
Expand Down
5 changes: 5 additions & 0 deletions dynaconf/contrib/django_dynaconf_v2.py
Expand Up @@ -69,6 +69,9 @@ def load(django_settings_module_name=None, **kwargs): # pragma: no cover
"default_settings_paths", dynaconf.DEFAULT_SETTINGS_FILES
)

class UserSettingsHolder(dynaconf.LazySettings):
_django_override = True

lazy_settings = dynaconf.LazySettings(**options)
dynaconf.settings = lazy_settings # rebind the settings

Expand Down Expand Up @@ -100,6 +103,8 @@ class Wrapper:
def __getattribute__(self, name):
if name == "settings":
return lazy_settings
if name == "UserSettingsHolder":
return UserSettingsHolder
return getattr(conf, name)

# This implementation is recommended by Guido Van Rossum
Expand Down
1 change: 1 addition & 0 deletions dynaconf/utils/functional.py
Expand Up @@ -31,6 +31,7 @@ class LazyObject:
# Avoid infinite recursion when tracing __init__.
_wrapped = None
_kwargs = None
_django_override = False

def __init__(self):
# Note: if a subclass overrides __init__(), it will likely need to
Expand Down
4 changes: 4 additions & 0 deletions example/django_example/foo/settings.py
Expand Up @@ -22,6 +22,9 @@
"ANOTHER_DRF_KEY": "VALUE",
}

# 596
TEST_VALUE = "a"
COLORS = ["black", "green"]

# HERE STARTS DYNACONF EXTENSION LOAD (Keep at the very bottom of settings.py)
# Read more at https://dynaconf.readthedocs.io/en/latest/guides/django.html
Expand All @@ -42,3 +45,4 @@
assert settings.PASSWORD == "My5up3r53c4et"
assert settings.get("PASSWORD") == "My5up3r53c4et"
assert settings.FOO == "It overrides every other env"
assert settings.TEST_VALUE == "a"
40 changes: 40 additions & 0 deletions example/django_example/polls/tests.py
@@ -1,11 +1,14 @@
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings

# Create your tests here.


@override_settings(ISSUE=596)
class SettingsTest(TestCase):
def test_settings(self):
self.assertEqual(settings.ISSUE, 596)
self.assertEqual(settings.SERVER, "prodserver.com")
# self.assertEqual(
# settings.STATIC_URL, "/changed/in/settings.toml/by/dynaconf/"
Expand Down Expand Up @@ -47,3 +50,40 @@ def test_settings(self):
self.assertEqual(settings.PASSWORD, "My5up3r53c4et")
self.assertEqual(settings.USERNAME, "admin_user_from_env")
self.assertEqual(settings.FOO, "It overrides every other env")

def test_override_settings_context(self):
self.assertEqual(settings.ISSUE, 596)
with self.settings(DAY=19):
self.assertEqual(settings.DAY, 19)


assert settings.TEST_VALUE == "a"


@override_settings(TEST_VALUE="c")
class TestOverrideClassDecoratorAndManager(TestCase):
def test_settings(self):
self.assertEqual(settings.TEST_VALUE, "c")

def test_modified_settings(self):
with self.settings(TEST_VALUE="b"):
self.assertEqual(settings.TEST_VALUE, "b")


class TestNoOverride(TestCase):
def test_settings(self):
self.assertEqual(settings.TEST_VALUE, "a")


class TestModifySettingsContextManager(TestCase):
def test_settings(self):
self.assertEqual(settings.COLORS, ["black", "green"])

with self.modify_settings(
COLORS={
"append": ["blue"],
"prepend": ["red"],
"remove": ["black"],
}
):
self.assertEqual(settings.COLORS, ["red", "green", "blue"])
Empty file.
23 changes: 23 additions & 0 deletions example/django_pure/foo/settings.py
@@ -0,0 +1,23 @@
import os

# Where is all the Django's settings?
# Take a look at ../settings.yaml and ../.secrets.yaml
# Dynaconf supports multiple formats that files can be toml, ini, json, py
# Files are also optional, dynaconf can read from envvars, Redis or Vault.

# Build paths inside the project like this: os.path.join(settings.BASE_DIR, ..)
# Or use the dynaconf helper `settings.path_for('filename')`
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

STATIC_URL = "/etc/foo/"


DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}

# 596
TEST_VALUE = "a"
31 changes: 31 additions & 0 deletions example/django_pure/foo/urls.py
@@ -0,0 +1,31 @@
"""foo URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import include
from django.urls import path

urlpatterns = [
path("polls/", include("polls.urls")),
path("admin/", admin.site.urls),
]

if settings.DEBUG:
import debug_toolbar

urlpatterns = [
path("__debug__/", include(debug_toolbar.urls)),
] + urlpatterns
15 changes: 15 additions & 0 deletions example/django_pure/foo/wsgi.py
@@ -0,0 +1,15 @@
"""
WSGI config for foo project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
"""
import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "foo.settings")

application = get_wsgi_application()
15 changes: 15 additions & 0 deletions example/django_pure/manage.py
@@ -0,0 +1,15 @@
#!/usr/bin/env python
import os
import sys

if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "foo.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
Empty file.
3 changes: 3 additions & 0 deletions example/django_pure/polls/admin.py
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions example/django_pure/polls/apps.py
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class PollsConfig(AppConfig):
name = "polls"
Empty file.
3 changes: 3 additions & 0 deletions example/django_pure/polls/models.py
@@ -0,0 +1,3 @@
from django.db import models

# Create your models here.
16 changes: 16 additions & 0 deletions example/django_pure/polls/tests.py
@@ -0,0 +1,16 @@
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings


assert settings.TEST_VALUE == "a"


@override_settings(TEST_VALUE="c")
class CheckSettings(TestCase):
def test_settings(self):
self.assertEqual(settings.TEST_VALUE, "c")

def test_modified_settings(self):
with self.settings(TEST_VALUE="b"):
self.assertEqual(settings.TEST_VALUE, "b")
5 changes: 5 additions & 0 deletions example/django_pure/polls/urls.py
@@ -0,0 +1,5 @@
from django.urls import path

from . import views

urlpatterns = [path("", views.index, name="index")]
6 changes: 6 additions & 0 deletions example/django_pure/polls/views.py
@@ -0,0 +1,6 @@
from django.conf import settings
from django.http import HttpResponse


def index(request):
return HttpResponse("Hello")

0 comments on commit d3b7575

Please sign in to comment.