Large code refactoring

* Add tests
* Remove git dependency (only needed when a git identifier is specified)
This commit is contained in:
David Wobrock authored and David-Wobrock committed 2017-11-29 23:57:23 +01:00
1 parent b8ca768a15
commit 50ce4f4cad
256 files changed
+1259 -473

No files matched your search

+142 -191
View File
@@ -13,135 +13,62 @@
# limitations under the License. # limitations under the License.
from __future__ import print_function from __future__ import print_function
import argparse
import logging as log import logging as log
import os import os
from io import StringIO
import re import re
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
import sys import sys
from . import utils
from .sql_analyser import analyse_sql_statements
def has_default(sql, **kwargs): class MigrationLinter(object):
if re.search('SET DEFAULT', sql) and kwargs['errors']:
err = next(err
for err in kwargs['errors']
if err['code'] == 'NOT_NULL')
if err:
log.info(
('Found a NOT_NULL error in migration, '
'but it has a default value added: {}').format(err))
kwargs['errors'].remove(err)
return False # Never fails
def clean_bytes_to_str(byte_input):
return byte_input.decode('utf-8').strip()
class MigrationLinter:
MIGRATION_FOLDER_NAME = 'migrations' MIGRATION_FOLDER_NAME = 'migrations'
migration_tests = ( def __init__(self, project_path, **kwargs):
{ # Verify correctness
'code': 'NOT_NULL', if not utils.is_directory(project_path):
'fn': lambda sql, **kw: re.search('NOT NULL', sql) and raise ValueError('The given path {0} does not seem to be a directory.'.format(project_path))
not re.search('CREATE TABLE', sql), if not utils.is_django_project(project_path):
'err_msg': 'NOT NULL constraint on columns' raise ValueError('The given path {0} does not seem to be a django project.'.format(project_path))
}, {
'code': 'DROP_COLUMN',
'fn': lambda sql, **kw: re.search('DROP COLUMN', sql),
'err_msg': 'DROPPING columns'
}, {
'code': 'RENAME_COLUMN',
'fn': lambda sql, **kw: re.search('ALTER TABLE .* CHANGE', sql) or
re.search('ALTER TABLE .* RENAME COLUMN', sql),
'err_msg': 'RENAMING columns'
}, {
'code': 'RENAME_TABLE',
'fn': lambda sql, **kw: re.search('RENAME TABLE', sql) or
re.search('ALTER TABLE .* RENAME TO', sql),
'err_msg': 'RENAMING tables'
}, {
'code': '',
'fn': has_default,
'err_msg': ''
}
)
def __init__(self, django_folder, commit_id=None, **kwargs): # Store parameters and options
self.location = django_folder self.django_path = project_path
self.commit_id = commit_id
self.ignore_name_contains = kwargs.get('ignore_name_contains', None) self.ignore_name_contains = kwargs.get('ignore_name_contains', None)
self.ignore_name = kwargs.get('ignore_name', None) or tuple() self.ignore_name = kwargs.get('ignore_name', None) or tuple()
self.include_apps = kwargs.get('include_apps', None) self.include_apps = kwargs.get('include_apps', None)
self.exclude_apps = kwargs.get('exclude_apps', None) self.exclude_apps = kwargs.get('exclude_apps', None)
self.database = kwargs.get('database', None) or 'default' self.database = kwargs.get('database', None) or 'default'
self.changed_migration_files = [] # Initialise counters
self._gather_migrations() self.nb_valid = 0
self.nb_ignored = 0
self.nb_erroneous = 0
self.nb_total = 0
def _gather_migrations(self): def lint_migration(self, app_name, migration_name):
# Find (one of) the initial commits print('({0}, {1})... '.format(app_name, migration_name), end='')
if not self.commit_id: self.nb_total += 1
git_init_cmd = 'cd {0} && git rev-list HEAD | tail -n 1'.format(
self.location)
process = Popen(git_init_cmd, shell=True, stdout=PIPE, stderr=PIPE)
for line in map(clean_bytes_to_str, process.stdout.readlines()):
self.commit_id = line
break
process.wait()
log.info('Operating until git identifier {0}'.format(self.commit_id))
# Get changes since specified commit
git_diff_command = 'cd {0} && git diff --name-only {1}'.format(
self.location, self.commit_id)
log.info('Executing {0}'.format(git_diff_command))
diff_process = Popen(
git_diff_command, shell=True, stdout=PIPE, stderr=PIPE)
for line in map(clean_bytes_to_str, diff_process.stdout.readlines()):
# Only gather lines that include migrations
if re.search(
'\/{0}\/.*\.py'.format(self.MIGRATION_FOLDER_NAME),
line) and \
'__init__' not in line:
self.changed_migration_files.append(line)
diff_process.wait()
if diff_process.returncode != 0:
output = []
for line in map(
clean_bytes_to_str, diff_process.stderr.readlines()):
output.append(line)
log.info("Error while git diff command:\n{}".format(
"".join(output)))
raise Exception('Error while executing git diff command')
def lint_migrations(self):
nb_valid = 0
nb_erroneous = 0
nb_ignore = 0
for migration in self.changed_migration_files:
app_name, migration_name = self._split_migration_path(migration)
print('{0}... '.format(migration), end='')
if self.should_ignore_migration(app_name, migration_name): if self.should_ignore_migration(app_name, migration_name):
print('IGNORE') print('IGNORE')
nb_ignore += 1 self.nb_ignore += 1
else: return
sql_statements = self.django_sqlmigrate(
sql_statements = self.get_sql(
app_name, migration_name) app_name, migration_name)
errors = [] analysis_result = analyse_sql_statements(
for statement in sql_statements: sql_statements)
is_valid, err = \ errors = analysis_result['errors']
self._test_sql_statement_for_backward_incompatibility(
statement, errors)
if not is_valid:
errors.append(err)
if not errors: if not errors:
print('OK') print('OK')
nb_valid += 1 self.nb_valid += 1
else: return
# Print errors
print('ERR') print('ERR')
nb_erroneous += 1 self.nb_erroneous += 1
for err in errors: for err in errors:
error_str = '\t{0}'.format(err['err_msg']) error_str = '\t{0}'.format(err['err_msg'])
if err['table']: if err['table']:
@@ -151,23 +78,104 @@ class MigrationLinter:
err['column']) err['column'])
error_str += ')' error_str += ')'
print(error_str) print(error_str)
print('*** Summary:')
print(('Valid migrations: {0}/{1} - '
'erroneous migrations: {2}/{1} - '
'ignored migrations: {3}/{1}').format(
nb_valid,
len(self.changed_migration_files),
nb_erroneous,
nb_ignore))
return nb_erroneous > 0
@classmethod def lint_all_migrations(self, git_commit_id=None):
def _split_migration_path(cls, migration_path): # Collect migrations
decomposed_path = split_path(migration_path) if git_commit_id:
for i, p in enumerate(decomposed_path): if not utils.is_git_project(self.django_path):
if p == cls.MIGRATION_FOLDER_NAME: raise ValueError('The given project {0} does not seem to be versioned by git.'.format(self.django_path))
return (decomposed_path[i-1], migrations = self._gather_migrations_git(git_commit_id)
os.path.splitext(decomposed_path[i+1])[0]) else:
migrations = self._gather_all_migrations()
# Lint those migrations
for m in migrations:
self.lint_migration(*m)
def print_summary(self):
print('*** Summary:')
print(('Valid migrations: {1}/{0} - '
'erroneous migrations: {2}/{0} - '
'ignored migrations: {3}/{0}').format(
self.nb_total,
self.nb_valid,
self.nb_erroneous,
self.nb_ignore))
@property
def has_errors(self):
return self.nb_erroneous > 0
def get_sql(self, app_name, migration_name):
""" It would be much faster to call the
command directly from the code using
call_command(), but requires the code
to setup django (by calling django.setup())
and set the DJANGO_SETTINGS_MODULE.
However, django is global and doesn't allow
multiple linter instances to exist at the same time.
(because they would all just lint the same django project)
Even if calling a shell is slow and ugly, for now,
it allows to seperate the instances correctly.
"""
git_diff_command = (
'cd {0} && '
'python manage.py sqlmigrate {1} {2} '
'--database {3}').format(
self.django_path, app_name, migration_name, self.database)
log.info('Executing {0}'.format(git_diff_command))
sqlmigrate_process = Popen(
git_diff_command, shell=True, stdout=PIPE, stderr=PIPE)
sql_statements = []
for line in map(
utils.clean_bytes_to_str, sqlmigrate_process.stdout.readlines()):
sql_statements.append(line)
sqlmigrate_process.wait()
if sqlmigrate_process.returncode != 0:
raise RuntimeError('sqlmigrate command failed')
log.info('Found {0} sql migration lines'.format(len(sql_statements)))
return sql_statements
def _gather_migrations_git(self, git_commit_id):
migrations = []
# Get changes since specified commit
git_diff_command = 'cd {0} && git diff --name-only {1}'.format(
self.django_path, git_commit_id)
log.info('Executing {0}'.format(git_diff_command))
diff_process = Popen(
git_diff_command, shell=True, stdout=PIPE, stderr=PIPE)
for line in map(utils.clean_bytes_to_str, diff_process.stdout.readlines()):
# Only gather lines that include migrations
if re.search(
'\/{0}\/.*\.py'.format(self.MIGRATION_FOLDER_NAME),
line) and \
'__init__' not in line:
app_name, migration_name = self._split_migration_path(line)
migrations.append((app_name, migration_name))
diff_process.wait()
if diff_process.returncode != 0:
output = []
for line in map(
utils.clean_bytes_to_str, diff_process.stderr.readlines()):
output.append(line)
log.info("Error while git diff command:\n{}".format(
"".join(output)))
raise Exception('Error while executing git diff command')
return migrations
def _gather_all_migrations(self):
migrations = []
for root, dirs, files in os.walk(self.django_path):
for file_name in files:
if os.path.basename(root) == self.MIGRATION_FOLDER_NAME and \
file_name.endswith('.py') and \
file_name != '__init__.py':
full_migration_path = os.path.join(root, file_name)
app_name, migration_name = self._split_migration_path(full_migration_path)
migrations.append((app_name, migration_name))
return migrations
def should_ignore_migration(self, app_name, migration_name): def should_ignore_migration(self, app_name, migration_name):
return (self.include_apps and return (self.include_apps and
@@ -178,73 +186,17 @@ class MigrationLinter:
self.ignore_name_contains in migration_name)\ self.ignore_name_contains in migration_name)\
or (migration_name in self.ignore_name) or (migration_name in self.ignore_name)
def django_sqlmigrate(self, app_name, migration_name): @classmethod
git_diff_command = ( def _split_migration_path(cls, migration_path):
'cd {0} && ' decomposed_path = utils.split_path(migration_path)
'python manage.py sqlmigrate {1} {2} ' for i, p in enumerate(decomposed_path):
'--database {3}').format( if p == cls.MIGRATION_FOLDER_NAME:
self.location, app_name, migration_name, self.database) return (decomposed_path[i-1],
log.info('Executing {0}'.format(git_diff_command)) os.path.splitext(decomposed_path[i+1])[0])
sqlmigrate_process = Popen(
git_diff_command, shell=True, stdout=PIPE, stderr=PIPE)
sql_statements = []
for line in map(
clean_bytes_to_str, sqlmigrate_process.stdout.readlines()):
if not line.startswith('--'): # Ignore comments
sql_statements.append(line)
sqlmigrate_process.wait()
if sqlmigrate_process.returncode != 0:
raise RuntimeError('sqlmigrate command failed')
log.info('Found {0} sql migration lines'.format(len(sql_statements)))
return sql_statements
def _test_sql_statement_for_backward_incompatibility(
self, sql_statement, errors):
for test in self.migration_tests:
if test['fn'](sql_statement, errors=errors):
log.info('Testing {0} -- ERROR'.format(sql_statement))
table_search = re.search(
'TABLE `([^`]*)`', sql_statement, re.IGNORECASE)
col_search = re.search(
'COLUMN `([^`]*)`', sql_statement, re.IGNORECASE)
err = {
'err_msg': test['err_msg'],
'code': test['code'],
'table': table_search.group(1) if table_search else None,
'column': col_search.group(1) if col_search else None
}
return False, err
log.info('Testing {0} -- PASSED'.format(sql_statement))
return True, None
def valid_folder(folder):
"""Verify folder exists,
folder is a django project
and folder is git versioned
"""
if not os.path.isdir(folder):
print("The passed argument doesn't seem to be a folder.")
return False
django_manage_file = os.path.join(folder, 'manage.py')
if not os.path.isfile(django_manage_file):
print(("The passed folder doesn't seem to be a "
"django project (no manage.py found)."))
return False
git_folder = os.path.join(folder, '.git')
if not os.path.isdir(git_folder):
print(("The passed folder doesn't seem to be "
"versioned by git (no .git/ folder found)."))
return False
return True
def split_path(path):
a, b = os.path.split(path)
return (split_path(a) if len(a) > 0 else []) + [b]
def _main(): def _main():
import argparse
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Detect backward incompatible django migrations.') description='Detect backward incompatible django migrations.')
parser.add_argument( parser.add_argument(
@@ -302,19 +254,18 @@ def _main():
log.basicConfig(format='%(message)s') log.basicConfig(format='%(message)s')
folder_name = args.django_folder[0] folder_name = args.django_folder[0]
if valid_folder(folder_name): # Create and use linter
linter = MigrationLinter( linter = MigrationLinter(
folder_name, folder_name,
args.commit_id,
ignore_name_contains=args.ignore_name_contains, ignore_name_contains=args.ignore_name_contains,
ignore_name=args.ignore_name, ignore_name=args.ignore_name,
include_apps=args.include_apps, include_apps=args.include_apps,
exclude_apps=args.exclude_apps, exclude_apps=args.exclude_apps,
database=args.database) database=args.database)
has_errors = linter.lint_migrations() linter.lint_all_migrations(
if has_errors: git_commit_id=args.commit_id)
sys.exit(1) linter.print_summary()
else: if linter.has_errors:
sys.exit(1) sys.exit(1)
+82
View File
@@ -0,0 +1,82 @@
# Copyright 2017 3YOURMIND GmbH
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
import logging as log
def has_default(sql, **kwargs):
if re.search('SET DEFAULT', sql) and kwargs['errors']:
err = next(err
for err in kwargs['errors']
if err['code'] == 'NOT_NULL')
if err:
log.info(
('Found a NOT_NULL error in migration, '
'but it has a default value added: {}').format(err))
kwargs['errors'].remove(err)
return False # Never fails
migration_tests = (
{
'code': 'NOT_NULL',
'fn': lambda sql, **kw: re.search('NOT NULL', sql) and
not re.search('CREATE TABLE', sql),
'err_msg': 'NOT NULL constraint on columns'
}, {
'code': 'DROP_COLUMN',
'fn': lambda sql, **kw: re.search('DROP COLUMN', sql),
'err_msg': 'DROPPING columns'
}, {
'code': 'RENAME_COLUMN',
'fn': lambda sql, **kw: re.search('ALTER TABLE .* CHANGE', sql) or
re.search('ALTER TABLE .* RENAME COLUMN', sql),
'err_msg': 'RENAMING columns'
}, {
'code': 'RENAME_TABLE',
'fn': lambda sql, **kw: re.search('RENAME TABLE', sql) or
re.search('ALTER TABLE .* RENAME TO', sql),
'err_msg': 'RENAMING tables'
}, {
'code': '',
'fn': has_default,
'err_msg': ''
}
)
def analyse_sql_statements(sql_statements):
errors = []
for statement in sql_statements:
for test in migration_tests:
if test['fn'](statement, errors=errors):
log.info('Testing {0} -- ERROR'.format(statement))
table_search = re.search(
'TABLE `([^`]*)`', statement, re.IGNORECASE)
col_search = re.search(
'COLUMN `([^`]*)`', statement, re.IGNORECASE)
err = {
'err_msg': test['err_msg'],
'code': test['code'],
'table': table_search.group(1) if table_search else None,
'column': col_search.group(1) if col_search else None
}
errors.append(err)
else:
log.info('Testing {0} -- PASSED'.format(statement))
return {
'errors': errors,
}
+54
View File
@@ -0,0 +1,54 @@
# Copyright 2017 3YOURMIND GmbH
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import print_function
import os
import re
def is_django_project(path):
django_manage_file = os.path.join(path, 'manage.py')
return os.path.isfile(django_manage_file)
def is_git_project(path):
git_directory = os.path.join(path, '.git')
return os.path.isdir(git_directory)
def is_directory(path):
return os.path.isdir(path)
def print_stderr(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def find_project_settings_module(path):
"""Explore path recursively to the first settings.py file
and translate the name to package notation 'mysite.settings'
"""
for root, dirs, files in os.walk(path):
for file_name in files:
if file_name == 'settings.py':
return os.path.join(root.replace(path, ''), file_name).replace('/', '.').rstrip('.py')
def split_path(path):
a, b = os.path.split(path)
return (split_path(a) if (len(a) > 0 and a != '/') else []) + [b]
def clean_bytes_to_str(byte_input):
return byte_input.decode('utf-8').strip()
+2 -6
View File
@@ -34,6 +34,8 @@ ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DEFAULT_PROJECT = os.path.join(
_FIXTURES_FOLDER, 'test_project_add_not_null_column_followed_by_default/') _FIXTURES_FOLDER, 'test_project_add_not_null_column_followed_by_default/')
MULTI_COMMIT_PROJECT = os.path.join( MULTI_COMMIT_PROJECT = os.path.join(
_FIXTURES_FOLDER, 'test_project_multi_commit/') _FIXTURES_FOLDER, 'test_project_multi_commit/')
CORRECT_PROJECT = os.path.join(
_FIXTURES_FOLDER, 'test_correct_project/')
NOT_DJANGO_GIT_PROJECT = os.path.join( NOT_DJANGO_GIT_PROJECT = os.path.join(
_FIXTURES_FOLDER, 'test_git_project/') _FIXTURES_FOLDER, 'test_git_project/')
@@ -42,12 +44,6 @@ NOT_GIT_DJANGO_PROJECT = os.path.join(
ALL_GIT_PROJECTS = ( ALL_GIT_PROJECTS = (
NOT_DJANGO_GIT_PROJECT, NOT_DJANGO_GIT_PROJECT,
ADD_NOT_NULL_COLUMN_PROJECT,
CREATE_TABLE_WITH_NOT_NULL_COLUMN_PROJECT,
DROP_COLUMN_PROJECT,
RENAME_COLUMN_PROJECT,
RENAME_TABLE_PROJECT,
ADD_NOT_NULL_COLUMN_FOLLOWED_BY_DEFAULT_PROJECT,
MULTI_COMMIT_PROJECT MULTI_COMMIT_PROJECT
) )
+4
View File
@@ -0,0 +1,4 @@
import unittest
class CallLinterFromCommandLineTest(unittest.TestCase):
pass
+11 -11
View File
@@ -66,19 +66,19 @@ class BackwardcompatibilityDetectionTest(unittest.TestCase):
fixtures.prepare_git_project(test_project_path) fixtures.prepare_git_project(test_project_path)
self._test_linter_finds_no_errors( self._test_linter_finds_no_errors(
test_project_path, test_project_path,
commit_id='44330921696018f8f1480df897b38310549982cb') commit_id='1021d98b5943db56122c1f848f371ddc38788d0f')
#def test_specify_git_hash_by_tag(self): def test_specify_git_hash_by_tag(self):
# test_project_path = fixtures.MULTI_COMMIT_PROJECT test_project_path = fixtures.MULTI_COMMIT_PROJECT
# fixtures.prepare_git_project(test_project_path) fixtures.prepare_git_project(test_project_path)
# self._test_linter_finds_no_errors(test_project_path, commit_id='tag1') self._test_linter_finds_no_errors(test_project_path, commit_id='tag1')
def _test_linter_finds_errors(self, path, commit_id=None): def _test_linter_finds_errors(self, path, commit_id=None):
linter = MigrationLinter(path, commit_id=commit_id) linter = MigrationLinter(path)
has_errors = linter.lint_migrations() linter.lint_all_migrations(git_commit_id=commit_id)
self.assertTrue(has_errors) self.assertTrue(linter.has_errors)
def _test_linter_finds_no_errors(self, path, commit_id=None): def _test_linter_finds_no_errors(self, path, commit_id=None):
linter = MigrationLinter(path, commit_id=commit_id) linter = MigrationLinter(path)
has_errors = linter.lint_migrations() linter.lint_all_migrations(git_commit_id=commit_id)
self.assertFalse(has_errors) self.assertFalse(linter.has_errors)
+23
View File
@@ -0,0 +1,23 @@
import unittest
from django_migration_linter import MigrationLinter
from tests import fixtures
class TestMultipleLinters(unittest.TestCase):
def test_multiple_linters(self):
l1 = MigrationLinter(fixtures.ADD_NOT_NULL_COLUMN_PROJECT)
l2 = MigrationLinter(fixtures.RENAME_COLUMN_PROJECT)
l3 = MigrationLinter(fixtures.CORRECT_PROJECT)
l1.lint_migration('test_app', '0002')
l2.lint_migration('test_app', '0002')
l3.lint_migration('test_app1', '0001')
l3.lint_migration('test_app1', '0002')
self.assertTrue(l1.has_errors)
self.assertTrue(l2.has_errors)
self.assertFalse(l3.has_errors)
self.assertEqual(l1.nb_total, 1)
self.assertEqual(l2.nb_total, 1)
self.assertEqual(l3.nb_total, 2)
@@ -0,0 +1,3 @@
*.pyc
__pycache__/
*sqlite3
@@ -0,0 +1,22 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
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?"
)
raise
execute_from_command_line(sys.argv)
Whitespace-only changes.
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class TestApp1Config(AppConfig):
name = 'test_app1'
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2017-11-27 15:49
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='A',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('null_field', models.IntegerField(null=True)),
],
),
]
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2017-11-27 15:49
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('test_app1', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='a',
name='new_null_field',
field=models.IntegerField(null=True),
),
]
@@ -0,0 +1,5 @@
from django.db import models
class A(models.Model):
null_field = models.IntegerField(null=True)
new_null_field = models.IntegerField(null=True)
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
Whitespace-only changes.
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class TestApp2Config(AppConfig):
name = 'test_app2'
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2017-11-29 21:08
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='B',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('null_field', models.IntegerField(null=True)),
],
),
]
@@ -0,0 +1,4 @@
from django.db import models
class B(models.Model):
null_field = models.IntegerField(null=True)
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
Whitespace-only changes.
@@ -0,0 +1,121 @@
"""
Django settings for test_project project.
Generated by 'django-admin startproject' using Django 1.11.6.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'gtb+mohs%gf0+#fr89vw7e&#edmv=q8!iv+n&j_soawk$tkj3^'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'test_app1',
'test_app2'
]
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 = 'test_project.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 = 'test_project.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django_fake_database_backends.backends.mysql'
}
}
# Password validation
# https://docs.djangoproject.com/en/1.11/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/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/'
@@ -0,0 +1,21 @@
"""linter_test_project URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf.urls import url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
]
@@ -0,0 +1,16 @@
"""
WSGI config for linter_test_project 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/1.11/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings")
application = get_wsgi_application()
@@ -1,22 +0,0 @@
All files
# Veuillez saisir le message de validation pour vos modifications. Les lignes
# commençant par '#' seront ignorées, et un message vide abandonne la validation.
# Sur la branche master
# Modifications qui seront validées :
# nouveau fichier: .gitignore
# nouveau fichier: manage.py
# nouveau fichier: test_app/__init__.py
# nouveau fichier: test_app/admin.py
# nouveau fichier: test_app/apps.py
# nouveau fichier: test_app/migrations/0001_create_table.py
# nouveau fichier: test_app/migrations/0002_add_new_not_null_field.py
# nouveau fichier: test_app/migrations/__init__.py
# nouveau fichier: test_app/models.py
# nouveau fichier: test_app/tests.py
# nouveau fichier: test_app/views.py
# nouveau fichier: test_project_add_not_null_column/__init__.py
# nouveau fichier: test_project_add_not_null_column/settings.py
# nouveau fichier: test_project_add_not_null_column/urls.py
# nouveau fichier: test_project_add_not_null_column/wsgi.py
#
@@ -1 +0,0 @@
ref: refs/heads/master
@@ -1,5 +0,0 @@
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
@@ -1 +0,0 @@
Unnamed repository; edit this file 'description' to name the repository.
@@ -1,6 +0,0 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
@@ -1,3 +0,0 @@
0000000000000000000000000000000000000000 13850658e796761657478d38eacd426e208b05b0 David-Wobrock <david.wobrock@gmail.com> 1509046620 +0200 commit (initial): Init
13850658e796761657478d38eacd426e208b05b0 0c78bfd0462272a51e3515731a6e6776bea72f72 David-Wobrock <david.wobrock@gmail.com> 1509046634 +0200 commit: All files
0c78bfd0462272a51e3515731a6e6776bea72f72 13ddff99f80afca39dc64eaa65928244584a88d3 David-Wobrock <david.wobrock@gmail.com> 1509046854 +0200 commit (amend): All files
@@ -1,3 +0,0 @@
0000000000000000000000000000000000000000 13850658e796761657478d38eacd426e208b05b0 David-Wobrock <david.wobrock@gmail.com> 1509046620 +0200 commit (initial): Init
13850658e796761657478d38eacd426e208b05b0 0c78bfd0462272a51e3515731a6e6776bea72f72 David-Wobrock <david.wobrock@gmail.com> 1509046634 +0200 commit: All files
0c78bfd0462272a51e3515731a6e6776bea72f72 13ddff99f80afca39dc64eaa65928244584a88d3 David-Wobrock <david.wobrock@gmail.com> 1509046854 +0200 commit (amend): All files
@@ -1,3 +0,0 @@
x]PËjÃ0ìÙ_±�ƒ�;Ôm øRrÈ­·„m­�ZY2zÒ¯¯dåY„˜™�™UÃee™?Ì }L¡•”‰~Öté«G¢¼£@U¤ÐaóU‹^B‘Eö RÀ2/^Ò"O—%O«¼Œ:% ¤³Æ*$Ø0JeÀ
æÌ‘pfœ×QÒÉ/£ÍY7°Þ…1)ô7à•QËk­a¦’«(»€óU�;L0Ãj|(‹£8¢ (Z†ÚŸ“ð89úí|à…ñ67o
Ýú{_&™&=ï�¨¬âu¼¸C;†œê*„a¸“˜Ñø¼U¶¶Fn½4©Ý‹´S­|錊␍µ:’o<ž�ÊmÅ~±Úºßs’T�ÔHB‰Ý&žÏï{øÐ$–s2UºFï„ÁUH÷‚)ãÿüájwr>Dqá£&
@@ -1,2 +0,0 @@
x¥�Á
B!E[û³�óL…ˆmú‚ÖŽÎ+)Äê÷3ú„–ç¹'ÖRr‡ÙmW½‰€a§mŠdtdö Å^\"¯�[XÄx4*<ûµ68†WN›såVã␍vé‹Óû‡‡K ù>ÅZö0[‘FX£FTcá.\¨Ó#wõ"";ã
@@ -1,3 +0,0 @@
x•ŽA
Â0E]çÙ‹ešf&SQð®ÓɨÅÖH�z}+âü»÷�'yûb]C‹2©ZñÞÍKNñ-A”V# 0
×,€ ÄÜâ¤×bë†YCK�jÂ৆5JòŽÔw€˜ø(ç<Ù}|öiuÈÝ”åb×éƒÕë‹ÛÓû¡’<nl�Ђ'j¼]‚0ó;‡ý[ÁøS˜Ý0Øc?èݼî¦Ib
@@ -1 +0,0 @@
xm�OOA Å=ϧhö‰îãE& Á(ÀxœvºË�Ît3S@¿½Ëžš¦¯ï÷Ú5ñîoŠ¢P_Ëé jŽ�o¡áä£`2‚YL—x‹µÀ¥–JÍð»ãŒdƒp^¶DvM6ƒ…ÀnGxG¸G‚½Mþ4Š6 ƒª²]G¾¶â9VUï÷Ú'{x8␍€coî34žð2¢ÚˆtùIkÇu.ÝÖÆ–ÿ2Õ4F=*G#½áƒ°vØÿŒ¢¹õZÏT>tœ8+Õ$pv)ë^ep´(æØ›«¨Jq.1î}âXf‡�Ý‘ ŠñÛË|º0ËÉj5›O—æc1þ|Ÿ·P\ÿÏXçLd1qGdj¦]8Ùˆ�m.†J]¡àþK0ª_òCš€
@@ -1 +0,0 @@
13ddff99f80afca39dc64eaa65928244584a88d3
@@ -1,23 +0,0 @@
All files
# Veuillez saisir le message de validation pour vos modifications. Les lignes
# commençant par '#' seront ignorées, et un message vide abandonne la validation.
# Sur la branche master
# Modifications qui seront validées :
# nouveau fichier: .gitignore
# nouveau fichier: manage.py
# nouveau fichier: test_app/__init__.py
# nouveau fichier: test_app/admin.py
# nouveau fichier: test_app/apps.py
# nouveau fichier: test_app/migrations/0001_create_table.py
# nouveau fichier: test_app/migrations/0002_add_new_not_null_field.py
# nouveau fichier: test_app/migrations/__init__.py
# nouveau fichier: test_app/migrations_utils.py
# nouveau fichier: test_app/models.py
# nouveau fichier: test_app/tests.py
# nouveau fichier: test_app/views.py
# nouveau fichier: test_project_add_not_null_column_followed_by_default/__init__.py
# nouveau fichier: test_project_add_not_null_column_followed_by_default/settings.py
# nouveau fichier: test_project_add_not_null_column_followed_by_default/urls.py
# nouveau fichier: test_project_add_not_null_column_followed_by_default/wsgi.py
#
@@ -1 +0,0 @@
ref: refs/heads/master
@@ -1,5 +0,0 @@
[core]
repositoryformatversion = 0
filemode = false
bare = false
logallrefupdates = true
@@ -1 +0,0 @@
Unnamed repository; edit this file 'description' to name the repository.
@@ -1,6 +0,0 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
@@ -1,4 +0,0 @@
0000000000000000000000000000000000000000 188d1e847da762e20ec43db374ffba8c5ee40581 David-Wobrock <david.wobrock@gmail.com> 1509048870 +0200 commit (initial): Init
188d1e847da762e20ec43db374ffba8c5ee40581 1ed2db048937886ab77c892d7ffa4bb3da6ae8a9 David-Wobrock <david.wobrock@gmail.com> 1509048882 +0200 commit: All files
1ed2db048937886ab77c892d7ffa4bb3da6ae8a9 4c2edb394b46664ac61f0b6fb29e57fe807ce21e David-Wobrock <david.wobrock@gmail.com> 1509049863 +0200 commit (amend): All files
4c2edb394b46664ac61f0b6fb29e57fe807ce21e 16fd55a08b9ad1af4282cafccf5c14adf1f2a1dc David-Wobrock <david.wobrock@gmail.com> 1509381987 +0100 commit (amend): All files
@@ -1,4 +0,0 @@
0000000000000000000000000000000000000000 188d1e847da762e20ec43db374ffba8c5ee40581 David-Wobrock <david.wobrock@gmail.com> 1509048870 +0200 commit (initial): Init
188d1e847da762e20ec43db374ffba8c5ee40581 1ed2db048937886ab77c892d7ffa4bb3da6ae8a9 David-Wobrock <david.wobrock@gmail.com> 1509048882 +0200 commit: All files
1ed2db048937886ab77c892d7ffa4bb3da6ae8a9 4c2edb394b46664ac61f0b6fb29e57fe807ce21e David-Wobrock <david.wobrock@gmail.com> 1509049863 +0200 commit (amend): All files
4c2edb394b46664ac61f0b6fb29e57fe807ce21e 16fd55a08b9ad1af4282cafccf5c14adf1f2a1dc David-Wobrock <david.wobrock@gmail.com> 1509381987 +0100 commit (amend): All files
@@ -1,3 +0,0 @@
x]PËjÃ0ìÙ_±�ƒ�;Ôm øRrÈ­·„m­�ZY2zÒ¯¯dåY„˜™�™UÃee™?Ì }L¡•”‰~Öté«G¢¼£@U¤ÐaóU‹^B‘Eö RÀ2/^Ò"O—%O«¼Œ:% ¤³Æ*$Ø0JeÀ
æÌ‘pfœ×QÒÉ/£ÍY7°Þ…1)ô7à•QËk­a¦’«(»€óU�;L0Ãj|(‹£8¢ (Z†ÚŸ“ð89úí|à…ñ67o
Ýú{_&™&=ï�¨¬âu¼¸C;†œê*„a¸“˜Ñø¼U¶¶Fn½4©Ý‹´S­|錊␍µ:’o<ž�ÊmÅ~±Úºßs’T�ÔHB‰Ý&žÏï{øÐ$–s2UºFï„ÁUH÷‚)ãÿüájwr>Dqá£&
@@ -1,3 +0,0 @@
x•ÎM
Â0†a×9Eöb™üOADÁ;¸N2S-¶FjÔëñ.ßoñðå2Ïc•ÚøU]˜%š”}Šˆ �í!xta }*iŸ ™,îqá[•
‘£␍ƒ×¬�³5”L°ÃИì˜-8T">ë¥,ò_#mN%-%_å–¾Ù½¹?Ïqœº\æ�Tz°ˆ¨å4€hk;Zù? ê14B5B¦IãÄñøK&
@@ -1 +0,0 @@
xm�OOA Å=ϧhö‰îãE& Á(ÀxœvºË�Ît3S@¿½Ëžš¦¯ï÷Ú5ñîoŠ¢P_Ëé jŽ�o¡áä£`2‚YL—x‹µÀ¥–JÍð»ãŒdƒp^¶DvM6ƒ…ÀnGxG¸G‚½Mþ4Š6 ƒª²]G¾¶â9VUï÷Ú'{x8␍€coî34žð2¢ÚˆtùIkÇu.ÝÖÆ–ÿ2Õ4F=*G#½áƒ°vØÿŒ¢¹õZÏT>tœ8+Õ$pv)ë^ep´(æØ›«¨Jq.1î}âXf‡�Ý‘ ŠñÛË|º0ËÉj5›O—æc1þ|Ÿ·P\ÿÏXçLd1qGdj¦]8Ùˆ�m.†J]¡àþK0ª_òCš€
@@ -1,2 +0,0 @@
x­‘1OÃ0…™ó+Nê�5Q\A@•2 UE ¬,YN|‰Œ;rì"þ=vÜ´…®x|~þÞ½s-u␍eYÞ, »Í Ñ\¨n ζÙcP’<£BÃ,r¨¿a÷ÉT§�ä„ä%h›‚<d¤È6÷@î¶E™´F÷@ië¬3H)ˆ~ÐÆ‚SÂÑJa=MŽI4ò‰—ózöõ¢óaB«q␍½prZ-eÃpm¤ÎŠ`k$Gx�Ë3*?‰«m’€?TU#p„
Þ'1\,Ó9(]CZ¡�A_ŸZVKLWëÉú1z« Óþ‚\$?q¾(ùò”R¦fT±«”¥ôp¢ªð‹*m©rRÒ6þØ&­Š+Ê_”ÅMLâØ2'mE޳F,À`pDs@:öþð~a?ˆËÍ}�]ľ1éðßÛµJÉEÉÕqÑ?8…ÆJ
Loaded 100 of 256 files, more files were not shown because too many files have changed in this diff. Show more