Not able to inspect Django migrations from test but working from Django command - django

I want to be able to inspect Django migrations within a test, so I can check basic sanity before running more expansive tests.
I was able to come up with the following in a Django command:
from django.core.management.base import BaseCommand
from django.db.migrations.loader import MigrationLoader
class Command(BaseCommand):
help = "Check migrations"
def handle(self, *args, **options):
self.migration_loader = MigrationLoader(None)
self.migration_loader.build_graph()
for root_node in self.migration_loader.graph.root_nodes():
app_name = root_node[0]
print(app_name, root_node[0])
But when I translate that into a test (using pytest):
from django.db.migrations.loader import MigrationLoader
def test_multiple_leaf_nodes_in_migration_graph():
migration_loader = MigrationLoader(None)
migration_loader.build_graph()
for root_node in migration_loader.graph.root_nodes():
app_name = root_node[0]
print(app_name, root_node[0])
Then the graph (root nodes) returns an empty list.
The project is structured as follows:
django_project/
settings.py
... # other Django core files
tests/
test_above.py
django_app/
models.py
... # other files from this app
management/commands/
command_above.py
pytest.ini
pytest.ini:
[pytest]
DJANGO_SETTINGS_MODULE = django_project.settings
python_files = tests.py test_*.py
The command:
pytest --no-migrations
PS: the whole point of this is to be able to detect migration errors without running them.
Is there anything I need to do so I can "see" migrations within the test?

pytest --no-migrations
Since you disabled migrations, the MigrationLoader won't load the migrations.
To load the migrations, you can override settings for MIGRATION_MODULES just for that test:
from django.test import override_settings
#override_settings(MIGRATION_MODULES={}) # Add this
def test_multiple_leaf_nodes_in_migration_graph():
...

Related

Django automatically performing the collectstatic command

In my project, I have a main static folder and a sub folder named static. When I make changes in my sub folder named static (which I specified in COLLECTSTATIC_DIRS within the settings file), I save the file and run the collectstatic command.
This successfully saves the changes, however is really inefficient as I am constantly changing css and Javascript files inside my project, which I store as static files.
I browsed the web, and came across a solution named whitenoise, which is a pip package. But this package only works for a short period of time, and after a few times of closing and opening my project folder, it completely stopped working.
Does anybody have another solution to deal with this problem? Thank you.
You can use python-watchdog and write your own Django command:
import time
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import BaseCommand
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
class Command(BaseCommand):
help = "Automatically calls collectstatic when the staticfiles get modified."
def handle(self, *args, **options):
event_handler = CollectstaticEventHandler()
observer = Observer()
for path in settings.STATICFILES_DIRS:
observer.schedule(event_handler, path, recursive=True)
observer.start()
try:
while True:
time.sleep(1)
finally:
observer.stop()
observer.join()
class CollectstaticEventHandler(FileSystemEventHandler):
def on_moved(self, event):
super().on_moved(event)
self._collectstatic()
def on_created(self, event):
super().on_created(event)
self._collectstatic()
def on_deleted(self, event):
super().on_deleted(event)
self._collectstatic()
def on_modified(self, event):
super().on_modified(event)
self._collectstatic()
def _collectstatic(self):
call_command("collectstatic", interactive=False)
You can use 3rd party solution which doesn't belong to Django to monitor your files and run commands on the files changes.
Take a look at fswatch utility Bash Script - fswatch trigger bash function
How to manage static files "Django-way"
Please check details on https://docs.djangoproject.com/en/3.0/howto/static-files/ what is a Django-way to do it correct :)
First of all set DEBUG = True while working on development.
Then add these lines to your project's urls.py:
from django.conf import settings
from django.views.decorators.cache import cache_control
from django.contrib.staticfiles.views import serve
from django.conf.urls.static import static
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL,
view=cache_control(no_cache=True, must_revalidate=True)(serve))
Here's an example using Python watchfiles and a Django management command.
# Lives in /my_app/manangement/commands/watch_files.py
import time
from django.conf import settings
from django.core.management import call_command
from django.core.management.base import BaseCommand
from watchfiles import watch
class Command(BaseCommand):
help = "Automatically calls collectstatic when the staticfiles get modified."
def handle(self, *args, **options):
print('WATCH_STATIC: Static file watchdog started.')
#for changes in watch([str(x) for x in settings.STATICFILES_DIRS]):
for changes in watch(*settings.STATICFILES_DIRS):
print(f'WATCH_STATIC: {changes}', end='')
call_command("collectstatic", interactive=False)
You can then run the Django management command in the background in whatever script you use to start Django.
python manage.py watch_static &
python manage.py runserver 0.0.0.0:8000

Flask - Attempted relative import in non-package

Following this tutorial on how to structure a Flask app, I have:
project/
__init__.py
app.py
models/
__init__.py
base.py
base.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
models/__init__.py
from .base import db
def init_app(app):
db.init_app(app)
project/__init__.py
from flask import Flask
def create_app()
from . import models, routes, services
app = Flask(__name__)
models.init_app(app)
# routes.init_app(app)
# services.init_app(app)
return app
finally, in app.py, I try to run it:
from . import create_app
app = create_app()
if __name__ == '__main__':
app.run(use_reloader=True, threaded=True, debug=True)
but I'm getting the error:
from . import create_app
ValueError: Attempted relative import in non-package
Am I building it right, what am I doing wrong?
I guess you are running your program by:
python project/app.py
In this case, you are not treat your "project" as a python package, which will raise the error you got. Instead, you can run your project with:
FLASK_APP=project.app flask run

django: data migrate permissions

I have a bunch of new permissions which I need to migrate. I tried doing it through data migration but complains about ContentType not being available.
Doing quick research I found out that ContentType table is populated after all the migrations applied.
I even tried using update_all_contenttypes() from from django.contrib.contenttypes.management import update_all_contenttypes
which causes migration to load data which is not consistent to the fixture.
What is the best way to migrate permission data in Django?
Here is a quick and dirty way to ensure all permissions for all apps have been created:
def add_all_permissions(apps=None, schema_editor=None):
from django.contrib.auth.management import create_permissions
if apps is None:
from django.apps import apps
for app_config in apps.get_app_configs():
app_config.models_module = True
create_permissions(app_config, verbosity=0)
app_config.models_module = None
class Migration(migrations.Migration):
dependencies = [('myapp', '0123_do_the_thing')]
operations = [
migrations.RunPython(add_all_permissions,
reverse_code=migrations.RunPython.noop)
# ...
]
NOTE: edited to include ruohola's excellent suggestion
There are 2 ways to solve this:
1) The ugly way:
Run manage.py migrate auth before your wanted migration
2) Recommended way:
from django.contrib.auth.management import create_permissions
def add_permissions(apps, schema_editor):
apps.models_module = True
create_permissions(apps, verbosity=0)
apps.models_module = None
# rest of code here....
Here are steps for adding custom permissions to the User model:
First create a migration file, for example under your authentication application,
Here i named it 0002_permission_fixtures.py:
account (your authentication application)
|_migrations
|__ 0001_initial.py
|__ 0002_permission_fixtures.py
|__ __init__.py
Then adding your permission objects, as follow:
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def forwards_func(apps, schema_editor):
# Get models that we needs them
user = apps.get_model("auth", "User")
permission = apps.get_model("auth", "Permission")
content_type = apps.get_model("contenttypes", "ContentType")
# Get user content type object
uct = content_type.objects.get_for_model(user)
db_alias = schema_editor.connection.alias
# Adding your custom permissions to User model:
permission.objects.using(db_alias).bulk_create([
permission(codename='add_sample', name='Can add sample', content_type=uct),
permission(codename='change_sample', name='Can change sample', content_type=uct),
permission(codename='delete_sample', name='Can delete sample', content_type=uct),
])
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '__latest__'),
]
operations = [
migrations.RunPython(
forwards_func,
),
]
To run this migration, first migrate contenttype model, and then migrate your application (here is account).
$ python manage.py migrate contenttypes
$ python manage.py migrate account

Fixtures not loaded during testing

I wrote a unit test checking whether initial data is loaded correctly. However the Node.objects.all().count() always returns 0, thus it seems as the fixtures are not loaded at all. There is no output/error msg in the command line that fixtures are not loaded.
from core.models import Node
class NodeTableTestCase(unittest.TestCase):
fixtures = ['core/core_fixture.json']
def setUp(self):
print "nothing to prepare..."
def testFixture(self):
"""Check if initial data can be loaded correctly"""
self.assertEqual(Node.objects.all().count(), 14)
the fixture core_fixture.json contains 14 nodes and I'm using this fixture as a initial data load into the db using the following command:
python manage.py loaddata core/core_fixture.json
They are located in the folder I provided in the settings.py setting FIXTURE_DIRS.
Found the solution in another thread, answer from John Mee
# Import the TestCase from django.test:
# Bad: import unittest
# Bad: import django.utils.unittest
# Good: import django.test
from django.test import TestCase
class test_something(TestCase):
fixtures = ['one.json', 'two.json']
...
Doing this I got a proper error message, saying that foreign key is violated and I had to also include the fixtures for the app "auth". I exported the needed data with this command:
manage.py dumpdata auth.User auth.Group > usersandgroups.json
Using Unittest I got only the message that loading of fixture data failed, which was not very helpful.
Finally my working test looks like this:
from django.test import TestCase
class NodeTableTestCase2(TestCase):
fixtures = ['auth/auth_usersandgroups_fixture.json','core/core_fixture.json']
def setUp(self):
# Test definitions as before.
print "welcome in setup: while..nothing to setup.."
def testFixture2(self):
"""Check if initial data can be loaded correctly"""
self.assertEqual(Node.objects.all().count(), 11)
When loading fixtures in test cases, I don't think Django allows you to include the directory name. Try changing your fixtures setting to:
fixtures = ['core_fixture.json',]
You might have to change your FIXTURE_DIRS setting as well, to include the core directory.
If you run your tests in verbose mode, you will see the fixture files that Django attempts to load. This should help you debug your configuration.
python manage.py test -v 2
Make sure you have your app listed in INSTALLED_APPS and that your app contains models.py file.

How to Unit test with different settings in Django?

Is there any simple mechanism for overriding Django settings for a unit test? I have a manager on one of my models that returns a specific number of the latest objects. The number of objects it returns is defined by a NUM_LATEST setting.
This has the potential to make my tests fail if someone were to change the setting. How can I override the settings on setUp() and subsequently restore them on tearDown()? If that isn't possible, is there some way I can monkey patch the method or mock the settings?
EDIT: Here is my manager code:
class LatestManager(models.Manager):
"""
Returns a specific number of the most recent public Articles as defined by
the NEWS_LATEST_MAX setting.
"""
def get_query_set(self):
num_latest = getattr(settings, 'NEWS_NUM_LATEST', 10)
return super(LatestManager, self).get_query_set().filter(is_public=True)[:num_latest]
The manager uses settings.NEWS_LATEST_MAX to slice the queryset. The getattr() is simply used to provide a default should the setting not exist.
EDIT: This answer applies if you want to change settings for a small number of specific tests.
Since Django 1.4, there are ways to override settings during tests:
https://docs.djangoproject.com/en/stable/topics/testing/tools/#overriding-settings
TestCase will have a self.settings context manager, and there will also be an #override_settings decorator that can be applied to either a test method or a whole TestCase subclass.
These features did not exist yet in Django 1.3.
If you want to change settings for all your tests, you'll want to create a separate settings file for test, which can load and override settings from your main settings file. There are several good approaches to this in the other answers; I have seen successful variations on both hspander's and dmitrii's approaches.
You can do anything you like to the UnitTest subclass, including setting and reading instance properties:
from django.conf import settings
class MyTest(unittest.TestCase):
def setUp(self):
self.old_setting = settings.NUM_LATEST
settings.NUM_LATEST = 5 # value tested against in the TestCase
def tearDown(self):
settings.NUM_LATEST = self.old_setting
Since the django test cases run single-threaded, however, I'm curious about what else may be modifying the NUM_LATEST value? If that "something else" is triggered by your test routine, then I'm not sure any amount of monkey patching will save the test without invalidating the veracity of the tests itself.
You can pass --settings option when running tests
python manage.py test --settings=mysite.settings_local
Although overriding settings configuration on runtime might help, in my opinion you should create a separate file for testing. This saves lot of configuration for testing and this would ensure that you never end up doing something irreversible (like cleaning staging database).
Say your testing file exists in 'my_project/test_settings.py', add
settings = 'my_project.test_settings' if 'test' in sys.argv else 'my_project.settings'
in your manage.py. This will ensure that when you run python manage.py test you use test_settings only. If you are using some other testing client like pytest, you could as easily add this to pytest.ini
Update: the solution below is only needed on Django 1.3.x and earlier. For >1.4 see slinkp's answer.
If you change settings frequently in your tests and use Python ≥2.5, this is also handy:
from contextlib import contextmanager
class SettingDoesNotExist:
pass
#contextmanager
def patch_settings(**kwargs):
from django.conf import settings
old_settings = []
for key, new_value in kwargs.items():
old_value = getattr(settings, key, SettingDoesNotExist)
old_settings.append((key, old_value))
setattr(settings, key, new_value)
yield
for key, old_value in old_settings:
if old_value is SettingDoesNotExist:
delattr(settings, key)
else:
setattr(settings, key, old_value)
Then you can do:
with patch_settings(MY_SETTING='my value', OTHER_SETTING='other value'):
do_my_tests()
You can override setting even for a single test function.
from django.test import TestCase, override_settings
class SomeTestCase(TestCase):
#override_settings(SOME_SETTING="some_value")
def test_some_function():
or you can override setting for each function in class.
#override_settings(SOME_SETTING="some_value")
class SomeTestCase(TestCase):
def test_some_function():
#override_settings is great if you don't have many differences between your production and testing environment configurations.
In other case you'd better just have different settings files. In this case your project will look like this:
your_project
your_app
...
settings
__init__.py
base.py
dev.py
test.py
production.py
manage.py
So you need to have your most of your settings in base.py and then in other files you need to import all everything from there, and override some options. Here's what your test.py file will look like:
from .base import *
DEBUG = False
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'app_db_test'
}
}
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher',
)
LOGGING = {}
And then you either need to specify --settings option as in #MicroPyramid answer, or specify DJANGO_SETTINGS_MODULE environment variable and then you can run your tests:
export DJANGO_SETTINGS_MODULE=settings.test
python manage.py test
For pytest users.
The biggest issue is:
override_settings doesn't work with pytest.
Subclassing Django's TestCase will make it work but then you can't use pytest fixtures.
The solution is to use the settings fixture documented here.
Example
def test_with_specific_settings(settings):
settings.DEBUG = False
settings.MIDDLEWARE = []
..
And in case you need to update multiple fields
def override_settings(settings, kwargs):
for k, v in kwargs.items():
setattr(settings, k, v)
new_settings = dict(
DEBUG=True,
INSTALLED_APPS=[],
)
def test_with_specific_settings(settings):
override_settings(settings, new_settings)
I created a new settings_test.py file which would import everything from settings.py file and modify whatever is different for testing purpose.
In my case I wanted to use a different cloud storage bucket when testing.
settings_test.py:
from project1.settings import *
import os
CLOUD_STORAGE_BUCKET = 'bucket_name_for_testing'
manage.py:
def main():
# use seperate settings.py for tests
if 'test' in sys.argv:
print('using settings_test.py')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project1.settings_test')
else:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project1.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)
Found this while trying to fix some doctests... For completeness I want to mention that if you're going to modify the settings when using doctests, you should do it before importing anything else...
>>> from django.conf import settings
>>> settings.SOME_SETTING = 20
>>> # Your other imports
>>> from django.core.paginator import Paginator
>>> # etc
I'm using pytest.
I managed to solve this the following way:
import django
import app.setting
import modules.that.use.setting
# do some stuff with default setting
setting.VALUE = "some value"
django.setup()
import importlib
importlib.reload(app.settings)
importlib.reload(modules.that.use.setting)
# do some stuff with settings new value
You can override settings in test in this way:
from django.test import TestCase, override_settings
test_settings = override_settings(
DEFAULT_FILE_STORAGE='django.core.files.storage.FileSystemStorage',
PASSWORD_HASHERS=(
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
)
)
#test_settings
class SomeTestCase(TestCase):
"""Your test cases in this class"""
And if you need these same settings in another file you can just directly import test_settings.
If you have multiple test files placed in a subdirectory (python package), you can override settings for all these files based on condition of presence of 'test' string in sys.argv
app
tests
__init__.py
test_forms.py
test_models.py
__init__.py:
import sys
from project import settings
if 'test' in sys.argv:
NEW_SETTINGS = {
'setting_name': value,
'another_setting_name': another_value
}
settings.__dict__.update(NEW_SETTINGS)
Not the best approach. Used it to change Celery broker from Redis to Memory.
One setting for all tests in a testCase
class TestSomthing(TestCase):
def setUp(self, **kwargs):
with self.settings(SETTING_BAR={ALLOW_FOO=True})
yield
override one setting in the testCase
from django.test import override_settings
#override_settings(SETTING_BAR={ALLOW_FOO=False})
def i_need_other_setting(self):
...
Important
Even though you are overriding these settings this will not apply to settings that your server initialize stuff with because it is already initialized, to do that you will need to start django with another setting module.