├── .gitignore ├── README.rst ├── django_compat_lint.py └── rules ├── __init__.py ├── django_13.py ├── django_14.py ├── django_15.py ├── django_16.py ├── django_17.py └── django_utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | *.egg-info 4 | docs/_build/ 5 | .coverage -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django_compat_lint -- check Django compatibility of your code 2 | =========================================================================== 3 | 4 | Django's API stability policy is nice, but there are still things that 5 | change from one version to the next. Figuring out all of those things 6 | when it's time to upgrade can be tediious and annoying as you flip 7 | back and forth between the release notes and your code, or start 8 | grepping for things in your code. 9 | 10 | So why not automate it? 11 | 12 | django_compat_lint, in the grand tradition of lint tools, is a simple 13 | and extensible engine for going through files of code line by line, 14 | applying some rules that look for potential problems, and then report 15 | those problems. As the name suggests, it is geared toward checking a 16 | Django codebase and finding potential issues you'd run into when 17 | upgrading to a more recent Django. 18 | 19 | 20 | How to use it 21 | ------------- 22 | 23 | Put simply:: 24 | 25 | python django_compat_lint.py [OPTIONS] [FILE1] [FILE2]... 26 | 27 | ``OPTIONS`` is a set of command-line options. There is one universal 28 | command-line option, implemented as ``-l`` or ``--level``, specifying 29 | the level of messages to report. See below for a definition of the 30 | message levels and what they mean. 31 | 32 | Beyond that, different options (run ``-h`` or ``--help`` to see a 33 | list) can be specified depending on what code-checking rules you have 34 | available. 35 | 36 | The output will be a series of messages, on stdout, each specifying 37 | its level, the file it came from, the line of code it came from, and 38 | the problem or suggestion that was noticed. 39 | 40 | Two useful shortcuts are available for specifying files to check: 41 | 42 | * If no files are specified, all ``.py`` files in the current working 43 | directory are checked. 44 | 45 | * A path to a directory can be specified; all ``.py`` files in that 46 | directory will be checked. 47 | 48 | Recursive checking involving ``os.walk()`` is left as an exercise for 49 | someone to send a pull request for. 50 | 51 | 52 | How it works 53 | ------------ 54 | 55 | django_compat_lint uses one or more sets of rules to check your 56 | code. A rule is simply a callable; it will be given the line of code 57 | to check, the name of the file that line came from, and an object 58 | representing the command-line options being used. It should return a 59 | 3-tuple of ``(warnings, errors, info)``, which are the supported 60 | levels of messages. Which levels are actually displayed is controlled 61 | by a command-line flag; these levels should be used for: 62 | 63 | ``warning`` 64 | Something that isn't going to immediately break the code, but may 65 | cause problems later. Deprecated APIs, for example, will issue 66 | warnings (since the APIs will still be usable for a couple Django 67 | versions). 68 | 69 | ``error`` 70 | Something that is going to immediately break your code if you try 71 | to run under a newer Django version. APIs and modules which have 72 | been removed are typical examples of this. 73 | 74 | ``info`` 75 | Something that doesn't and won't break your code, but is an 76 | outdated idiom or something which can be accomplished in a better 77 | way using more recent Django. 78 | 79 | 80 | Registering rules 81 | ----------------- 82 | 83 | Rules live in the ``rules/`` subdirectory, and a set of rules is simply 84 | a Python module which exports a variable named ``rules``. This should 85 | be a list of dictionaries, one per rule. Each dictionary should have 86 | the following keys. The first five correspond exactly to the 87 | same-named arguments to ``parser.add_option()`` in Python's 88 | ``optparse`` module (which implements the parsing of command-line 89 | flags): 90 | 91 | ``long_option`` 92 | The (long) command-line flag for this rule. To avoid conflicts, 93 | rules cannot use short flags. 94 | 95 | ``action`` 96 | What action to take with the flag. 97 | 98 | ``dest`` 99 | Similarly, where to store the value of the command-line flag. 100 | 101 | ``help`` 102 | A brief description of the rule and what it checks, for help 103 | output. 104 | 105 | The remaining keys are: 106 | 107 | ``callback`` 108 | The callback which implements the rule. 109 | 110 | ``enabled`` 111 | A callable which is passed the command-line options, and returns a 112 | boolean indicating, from those options, whether this rule is 113 | enabled. 114 | 115 | 116 | A simple example 117 | ---------------- 118 | 119 | Suppose that a new version of Django introduces a model field type 120 | called ``SuperAwesomeTextField``, which is just like ``TextField`` but 121 | better. So people who are upgrading may want to change from 122 | ``TextField`` to ``SuperAwesomeTextField``. A simple rule for this 123 | might live in a file named ``superawesomefield.py``. First, the 124 | callback for the rule:: 125 | 126 | def check_superawesomefield(line, filename, options): 127 | info = [] 128 | if filename == 'models.py' and 'TextField' in line: 129 | info.append('Consider using SuperAwesomeField instead of TextField.') 130 | return []. [], info 131 | 132 | This checks for the filename 'models.py' since a model field change is 133 | probably only applicable to models files. And it checks for use of the 134 | model ``TextField``, by just seeing if that appears in the line of 135 | code. More complex things might use regular expressions or other 136 | tricks to check a line. 137 | 138 | Since it's only ever going to give an "info"-level message, the 139 | "warnings" and "errors" lists are just always empty. 140 | 141 | Then, at the bottom of the file, the rule gets registered:: 142 | 143 | rules = [ 144 | {'option': '-a', 145 | 'long_option': '--superawesomefield', 146 | 'action': 'store_true', 147 | 'dest': 'superawesomefield', 148 | 'help': 'Check for places where SuperAwesomeField could be used.', 149 | 'callback': check_superawesomefield, 150 | 'enabled': lambda options: options.superawesomefield,} 151 | ] 152 | 153 | And that's it -- the engine will pick up that rule, and enable it 154 | whenever the appropriate command-line flag is used. -------------------------------------------------------------------------------- /django_compat_lint.py: -------------------------------------------------------------------------------- 1 | from optparse import OptionParser 2 | import os 3 | 4 | try: 5 | from importlib import import_module 6 | except ImportError: 7 | try: 8 | from django.utils.importlib import import_module 9 | except ImportError: 10 | raise 11 | 12 | 13 | RULES = [] 14 | 15 | usage = 'usage: %prog OPTIONS file1 file2 ...' 16 | 17 | MESSAGE_TEMPLATE = '%s:%s:%s:%s' 18 | 19 | 20 | def register_rules(rules_file): 21 | global RULES 22 | module = import_module('rules.%s' % rules_file) 23 | RULES += module.rules 24 | 25 | 26 | def setup(): 27 | self_dir = os.path.abspath(os.path.dirname(__file__)) 28 | rules_dir = os.path.join(self_dir, 'rules') 29 | for f in os.listdir(rules_dir): 30 | if f != '__init__.py' and '.pyc' not in f: 31 | register_rules(f.replace('.py', '')) 32 | 33 | 34 | def format_messages(messages, line, filename, level): 35 | return [MESSAGE_TEMPLATE % (level, filename, line, message) for \ 36 | message in messages] 37 | 38 | 39 | def check_file(filename, enabled_rules, options): 40 | messages = [] 41 | lines = open(os.path.abspath(filename)).readlines() 42 | for i, line in enumerate(lines): 43 | for rule in enabled_rules: 44 | warnings, errors, info = rule(line, filename, options) 45 | if options.level in ('warnings', 'all'): 46 | messages += format_messages(warnings, i + 1, filename, 'WARNING') 47 | if options.level in ('errors', 'all'): 48 | messages += format_messages(errors, i + 1, filename, 'ERROR') 49 | if options.level in ('info', 'all'): 50 | messages += format_messages(info, i + 1, filename, 'INFO') 51 | return messages 52 | 53 | 54 | if __name__ == '__main__': 55 | setup() 56 | 57 | parser = OptionParser(usage) 58 | parser.add_option('-l', '--level', dest='level', 59 | action='store', 60 | type='string', 61 | default='errors', 62 | help="Level of messages to display. 'errors', 'warnings', 'info' or 'all'. Default 'errors'.") 63 | 64 | for rule in RULES: 65 | parser.add_option(rule['long_option'], 66 | dest=rule['dest'], action=rule['action'], 67 | help=rule['help']) 68 | 69 | options, args = parser.parse_args() 70 | 71 | enabled_rules = [] 72 | for rule in RULES: 73 | if rule['enabled'](options): 74 | enabled_rules.append(rule['callback']) 75 | 76 | files = [] 77 | if not args: 78 | files += [path for path in os.listdir(os.getcwd()) if \ 79 | (os.path.isfile(path) and '.pyc' not in path)] 80 | else: 81 | for path in args: 82 | if os.path.isdir(os.path.abspath(path)): 83 | subdir = os.path.abspath(path) 84 | files += [os.path.join(subdir, f) for f in os.listdir(subdir) if \ 85 | (os.path.isfile(os.path.join(subdir, f)) and '.pyc' not in f)] 86 | else: 87 | files.append(path) 88 | 89 | messages = [] 90 | for filename in files: 91 | messages += check_file(filename, enabled_rules, options) 92 | 93 | for message in messages: 94 | print message 95 | -------------------------------------------------------------------------------- /rules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubernostrum/django-compat-lint/098e06c614bf3670b31bae850dde305634bb26f4/rules/__init__.py -------------------------------------------------------------------------------- /rules/django_13.py: -------------------------------------------------------------------------------- 1 | def check_forms(line, filename, options): 2 | info = [] 3 | if filename == 'forms.py': 4 | if 'PasswordInput' in line and 'render_value=False' in line: 5 | info.append("PasswordInput no long requires explicit render_value=False.") 6 | if 'FileInput' in line: 7 | info.append('Consider using ClearableFileInput instead of FileInput.') 8 | return [], [], info 9 | 10 | 11 | def check_models(line, filename, options): 12 | warnings = [] 13 | if filename == 'models.py': 14 | if 'XMLField' in line: 15 | warnings.append('XMLField is deprecated in Django 1.3. Use TextField instead.') 16 | if 'verify_exists' in line: 17 | warnings.append('verify_exists is deprecated in Django 1.3.') 18 | return warnings, [], [] 19 | 20 | 21 | def check_generic_views(line, filename, options): 22 | warnings = [] 23 | if 'django.views.generic' in line and \ 24 | ('create_update' in line or \ 25 | 'date_based' in line or \ 26 | 'list_detail' in line or \ 27 | 'simple' in line): 28 | warnings.append('Function-based generic views are deprecated in Django 1.3.') 29 | return warnings, [], [] 30 | 31 | 32 | def check_localflavor(line, filename, options): 33 | warnings = [] 34 | if filename == 'forms.py': 35 | if 'USStateField' in line and 'choices' not in line: 36 | warnings.append('USStateField default choices have changed. Consider setting explicit choices.') 37 | return warnings, [], [] 38 | 39 | 40 | def check_profanities_list(line, filename, options): 41 | warnings = [] 42 | if 'PROFANITIES_LIST' in line: 43 | warnings.append('Use of PROFANITIES_LIST is discouraged; the default is now an empty value.') 44 | return warnings, [], [] 45 | 46 | 47 | def check_response_template(line, filename, options): 48 | warnings = [] 49 | if 'test' in filename: 50 | if 'resp' in line and '.template' in line: 51 | warnings.append('TestClient.response.template is deprecated in Django 1.3. Use TestClient.response.templates instead.') 52 | return warnings, [], [] 53 | 54 | 55 | def check_compatcookie(line, filename, options): 56 | warnings = [] 57 | if 'django.http' in line and 'CompatCookie' in line: 58 | warnings.appened('django.http.CompatCookie is deprecated in Django 1.3. Use django.http.SimpleCookie instead.') 59 | return warnings, [], [] 60 | 61 | 62 | def check_redirect_next_param(line, filename, options): 63 | warnings = [] 64 | 65 | if 'request.GET.get' in line and 'next' in line: 66 | warnings.append("Potentially unsafe use of 'next' parameter from querystring.") 67 | return warnings, [], [] 68 | 69 | def check_django_13(line, filename, options): 70 | warnings = [] 71 | info = [] 72 | errors = [] 73 | 74 | for check in (check_forms, check_models, check_generic_views, 75 | check_localflavor, check_profanities_list, 76 | check_response_template, check_compatcookie): 77 | warn, err, inf = check(line, filename, options) 78 | warnings += warn 79 | info += inf 80 | errors += err 81 | 82 | return warnings, errors, info 83 | 84 | 85 | rules = [ 86 | {'long_option': '--django-13', 87 | 'action': 'store_true', 88 | 'dest': 'django_13', 89 | 'help': 'Check for changes and deprecations in Django 1.3.', 90 | 'callback': check_django_13, 91 | 'enabled': lambda options: options.django_13,} 92 | ] 93 | -------------------------------------------------------------------------------- /rules/django_14.py: -------------------------------------------------------------------------------- 1 | def check_operationalerror(line, filename, options): 2 | errors = [] 3 | if ('mysql' in line or 'MySQL' in line) and 'OperationalError' in line: 4 | errors.append('MySQLdb.OperationalError is no longer used in Django. Use django.db.utils.DatabaseError instead.') 5 | return [], errors, [] 6 | 7 | 8 | def check_template_loaders(line, filename, options): 9 | errors = [] 10 | if 'django.core' in line and 'template_loaders' in line: 11 | errors.append('django.core.template_loaders alias has been removed; use django.template.loader instead.') 12 | return [], errors, [] 13 | 14 | 15 | def check_filestorage_mixin(line, filename, options): 16 | errors = [] 17 | if 'open' in line and 'mixin=' in line: 18 | errors.append("The 'mixin' argument to Storage.open() has been removed.") 19 | return [], errors, [] 20 | 21 | 22 | def check_urlconf_imports(line, filename, options): 23 | warnings = [] 24 | if 'django.conf.urls.defaults' in line: 25 | warnings.append('django.conf.urls.defaults is deprecated; use django.conf.urls instead.') 26 | return warnings, [], [] 27 | 28 | 29 | def check_setup_environ(line, filename, options): 30 | warnings = [] 31 | if 'setup_environ' in line: 32 | warnings.append('setup_environ() is deprecated; set DJANGO_SETTINGS_MODULE or use settings.configure() instead.') 33 | return warnings, [], [] 34 | 35 | 36 | def check_filter_attrs(line, filename, options): 37 | errors = [] 38 | if '.is_safe' in line or '.needs_autoescape' in line: 39 | errors.append("The 'is_safe' and 'needs_autoescape' attributes of filters are deprecated; use keyword arguments to filter() instead.") 40 | return [], errors, [] 41 | 42 | 43 | def check_raw_post_data(line, filename, options): 44 | warnings = [] 45 | if 'raw_post_data' in line: 46 | warnings.append("The 'raw_post_data' attribute of HttpRequest is deprecated; use the 'body' attribute instead.") 47 | return warnings, [], [] 48 | 49 | 50 | def check_django_14(line, filename, options): 51 | warnings = [] 52 | info = [] 53 | errors = [] 54 | 55 | for check in (check_operationalerror, check_template_loaders, 56 | check_filestorage_mixin, check_urlconf_imports, 57 | check_setup_environ, check_filter_attrs, 58 | check_raw_post_data): 59 | warn, err, inf = check(line, filename, options) 60 | warnings += warn 61 | info += inf 62 | errors += err 63 | 64 | return warnings, errors, info 65 | 66 | 67 | rules = [ 68 | {'long_option': '--django-14', 69 | 'action': 'store_true', 70 | 'dest': 'django_14', 71 | 'help': 'Check for changes and deprecations in Django 1.4.', 72 | 'callback': check_django_14, 73 | 'enabled': lambda options: options.django_14,} 74 | ] 75 | -------------------------------------------------------------------------------- /rules/django_15.py: -------------------------------------------------------------------------------- 1 | def check_password_hasher(line, filename, options): 2 | warnings = [] 3 | if 'PASSWORD_HASHERS' in line: 4 | warnings.append('Custom password hashers are now required to accept Unicode strings.') 5 | return warnings, [], [] 6 | 7 | 8 | def check_simplejson(line, filename, options): 9 | warnings = [] 10 | if 'simplejson' in line: 11 | warnings.append("Use of the standard-library 'json' module is strongly recommended instead of simplejson.") 12 | return warnings, [], [] 13 | 14 | 15 | def check_transactiontestcase(line, filename, options): 16 | warnings = [] 17 | if 'TransactionTestCase' in line: 18 | warnings.append("TransactionTestCase behavior has changed; check that tests do not rely on hard-coded primary keys, sequence resets, or on the database being reset before each test run.") 19 | return warnings, [], [] 20 | 21 | 22 | def check_cleaned_data(line, filename, options): 23 | warnings = [] 24 | if 'if' in line and 'cleaned_data' in line: 25 | warnings.append("Checking the existence of cleaned_data to determine validity is no longer supported. Test on is_valid() instead.") 26 | return warnings, [], [] 27 | 28 | 29 | def check_slugify(line, filename, options): 30 | warnings = [] 31 | if 'django.template.defaultfilters' in line and 'slugify' in line: 32 | warnings.append("slugify() is now available as a standalone function in django.utils.text.") 33 | return warnings, [], [] 34 | 35 | 36 | def check_localflavor(line, filename, options): 37 | warnings = [] 38 | if 'django.contrib' in line and 'localflavor' in line: 39 | warnings.append("django.contrib.localflavor is deprecated in favor of the django-localflavor package.") 40 | return warnings, [], [] 41 | 42 | 43 | def check_depth(line, filename, options): 44 | warnings = [] 45 | if 'select_related' in line and 'depth' in line: 46 | warnings.append("The 'depth' parameter to select_related is deprecated. Use field names instead.") 47 | return warnings, [], [] 48 | 49 | 50 | def check_django_15(line, filename, options): 51 | warnings = [] 52 | info = [] 53 | errors = [] 54 | 55 | for check in (check_password_hasher, check_simplejson, 56 | check_transactiontestcase, check_cleaned_data, 57 | check_slugify, check_localflavor, 58 | check_depth): 59 | warn, err, inf = check(line, filename, options) 60 | warnings += warn 61 | info += inf 62 | errors += err 63 | 64 | return warnings, errors, info 65 | 66 | 67 | rules = [ 68 | {'long_option': '--django-15', 69 | 'action': 'store_true', 70 | 'dest': 'django_15', 71 | 'help': 'Check for changes and deprecations in Django 1.5.', 72 | 'callback': check_django_15, 73 | 'enabled': lambda options: options.django_15,} 74 | ] 75 | -------------------------------------------------------------------------------- /rules/django_16.py: -------------------------------------------------------------------------------- 1 | test_filenames_seen = [] 2 | 3 | def check_transaction_use(line, filename, options): 4 | warnings = [] 5 | if 'transaction' in line: 6 | warnings.append('Transaction handling behavior has changed in 1.6; please review use of transactions.') 7 | return warnings, [], [] 8 | 9 | 10 | def check_tests_init(line, filename, options): 11 | errors = [] 12 | global test_filenames_seen 13 | if '__init__' in filename and 'tests' in filename: 14 | if filename not in test_filenames_seen: 15 | errors.append("Django's test runner no longer finds tests in __init__.py files.") 16 | test_filenames_seen.append(filename) 17 | return [], errors, [] 18 | 19 | 20 | def check_user_override_in_tests(line, filename, options): 21 | warnings = [] 22 | if 'override_settings' in line and 'AUTH_USER_MODEL' in line: 23 | warnings.append('You must now import your custom user model in test files which will swamp it in.') 24 | return warnings, [], [] 25 | 26 | 27 | def check_dates(line, filename, options): 28 | warnings = [] 29 | if '.dates(' in line: 30 | warnings.append("Behavior of dates() has changed; please see release notes and investigate use of datetimes() instead.") 31 | return warnings, [], [] 32 | 33 | 34 | def check_date_hierarchy(line, filename, options): 35 | info = [] 36 | if 'date_hierarchy' in line: 37 | info.append("date_hierarchy now uses datetimes() and requires USE_TZ and time zone definitions.") 38 | return [], [], info 39 | 40 | 41 | def check_booleanfield_default(line, filename, options): 42 | warnings = [] 43 | if 'BooleanField' in line and 'default' not in line: 44 | warnings.append('BooleanField no longer has an implicit default; set an explicit default if one is needed.') 45 | return warnings, [], [] 46 | 47 | 48 | def check_empty_qs(line, filename, options): 49 | errors = [] 50 | if 'EmptyQuerySet(' in line: 51 | errors.append("EmptyQuerySet can't be directly instantiated; use a none() method of a manager instead.") 52 | return [], errors, [] 53 | 54 | 55 | def check_cycle_firstof(line, filename, options): 56 | info = [] 57 | if '{% cycle' in line or '{% firstof' in line: 58 | info.append("The behavior of the 'cycle' and 'firstof' template tags will change by Django 1.8.") 59 | return [], [], info 60 | 61 | 62 | def check_module_name(line, filename, options): 63 | warnings = [] 64 | if '.module_name' in line: 65 | warnings.append("The meta 'module_name' attribute is deprecated; use 'model_name' instead.") 66 | return warnings, [], [] 67 | 68 | 69 | def check_get_query_set(line, filename, options): 70 | warnings = [] 71 | if 'get_query_set(' in line or '.queryset(' in line: 72 | warnings.append("Methods which return querysets have all been standardized to 'get_queryset()'.") 73 | return warnings, [], [] 74 | 75 | 76 | def check_django_16(line, filename, options): 77 | warnings = [] 78 | info = [] 79 | errors = [] 80 | 81 | for check in (check_transaction_use, check_tests_init, 82 | check_user_override_in_tests, check_dates, 83 | check_date_hierarchy, check_booleanfield_default, 84 | check_empty_qs, check_cycle_firstof, 85 | check_module_name, check_get_query_set): 86 | warn, err, inf = check(line, filename, options) 87 | warnings += warn 88 | info += inf 89 | errors += err 90 | 91 | return warnings, errors, info 92 | 93 | 94 | rules = [ 95 | {'long_option': '--django-16', 96 | 'action': 'store_true', 97 | 'dest': 'django_16', 98 | 'help': 'Check for changes and deprecations in Django 1.6.', 99 | 'callback': check_django_16, 100 | 'enabled': lambda options: options.django_16,} 101 | ] 102 | -------------------------------------------------------------------------------- /rules/django_17.py: -------------------------------------------------------------------------------- 1 | migration_filenames_seen = [] 2 | 3 | def check_migrations(line, filename, options): 4 | info = [] 5 | global migration_filenames_seen 6 | if ('migrations' in filename) or \ 7 | ('import' in line and 'south' in line): 8 | if filename not in migration_filenames_seen: 9 | info.append("Django now includes a migration framework. " 10 | "Ensure your pre-existing migration files " 11 | "do not conflict with this.") 12 | migration_filenames_seen.append(filename) 13 | return [], [], info 14 | 15 | 16 | def check_allow_syncdb(line, filename, options): 17 | warnings = [] 18 | if 'allow_syncdb' in line: 19 | warnings.append("Use of 'allow_syncdb' is deprecated in favor " 20 | "of 'allow_migrate.") 21 | return warnings, [], [] 22 | 23 | 24 | def check_wsgi_handler(line, filename, options): 25 | errors = [] 26 | if 'django.core.handlers.wsgi.WSGIHandler' in line: 27 | errors.append("Direct use of 'django.core.handlers.wsgi.WSGIHandler' " 28 | "will now raise AppRegistryNotReady. Instead, use " 29 | "django.core.wsgi.get_wsgi_application().") 30 | return [], errors, [] 31 | 32 | 33 | def check_appcommand(line, filename, options): 34 | errors = [] 35 | if 'handle_app' in line and 'handle_app_config' not in line: 36 | errors.append("AppCommand subclasses must now implement the method " 37 | "'handle_app_config()' instead of 'handle_app()'.") 38 | return [], errors, [] 39 | 40 | 41 | def check_db_manager(line, filename, options): 42 | warnings = [] 43 | if 'db_manager' in line and 'using=None' in line: 44 | warnings.append("'db_manager(using=None)' behavior has changed and now " 45 | "retains rather than overrides previous manual DB " 46 | "routing choices.") 47 | return warnings, [], [] 48 | 49 | 50 | def check_remove_clear(line, filename, options): 51 | warnings = [] 52 | if 'remove(' in line or 'clear(' in line: 53 | warnings.append("The behavior of the 'remove()' and 'clear()' methods " 54 | "of related managers has changed. See the release notes.") 55 | return warnings, [], [] 56 | 57 | 58 | def check_select_for_update(line, filename, options): 59 | warnings = [] 60 | if 'select_for_update(' in line: 61 | warnings.append("The 'select_for_update()' method now requires a " 62 | "transaction or will raise an exception.") 63 | return warnings, [], [] 64 | 65 | 66 | def check_fileuploadhandler(line, filename, options): 67 | warnings = [] 68 | if 'def' in line and 'new_file(' in line: 69 | warnings.append("The 'new_file()' method of file upload handlers now " 70 | "must accept a 'content_type_extra' parameter.") 71 | return warnings, [], [] 72 | 73 | 74 | def check_abstract_user(line, filename, options): 75 | warnings = [] 76 | if 'class' in line and 'AbstractUser' in line: 77 | warnings.append("AbstractUser no longer defines a 'get_absolute_url' " 78 | "method. Define one yourself if it is required.") 79 | return warnings, [], [] 80 | 81 | 82 | def check_selectdatewidget(line, filename, options): 83 | errors = [] 84 | if 'SelectDateWidget' in line and 'required' in line: 85 | errors.append("SelectDateWidget no longer supports a 'required' " 86 | "argument. Use 'is_required' on the associated Field " 87 | "instead.") 88 | return [], errors, [] 89 | 90 | 91 | def check_is_hidden(line, filename, options): 92 | warnings = [] 93 | if 'is_hidden' in line and '=' in line: 94 | warnings.append("The 'is_hidden' property of widgets is now read-only. " 95 | "Use 'input_type = hidden' on a Widget subcless to " 96 | "create a hidden widget.") 97 | return warnings, [], [] 98 | 99 | 100 | def check_init_connection_state(line, filename, options): 101 | warnings = [] 102 | if 'init_connection_state(' in line: 103 | warnings.append("The 'init_connection_state()' method of database " 104 | "backends now executes in autocommit mode. Ensure " 105 | "your backend is compatible with this.") 106 | return warnings, [], [] 107 | 108 | 109 | def check_auto_primary_key(line, filename, options): 110 | warnings = [] 111 | if 'allows_primary_key_0' in line: 112 | warnings.append("The 'allows_primary_key_0' attribute has been renamed " 113 | "to 'allows_auto_pk_0'. Check the release notes for " 114 | "explanation of what it means.") 115 | return warnings, [], [] 116 | 117 | 118 | def check_language_supported(line, filename, options): 119 | errors = [] 120 | if ('get_language_from_path(' in line or 121 | 'get_supported_language_variant(' in line) and \ 122 | 'supported=' in line: 123 | errors.append("The 'get_language_from_path() and " 124 | "get_supported_language_variant() functions no longer " 125 | "accept the 'supported' argument.") 126 | return [], errors, [] 127 | 128 | 129 | def check_check(line, filename, options): 130 | errors = [] 131 | if ('models' in filename or 'managers' in filename or 132 | 'fields' in filename) and \ 133 | 'def check(' in line: 134 | errors.append("The system check framework now reserves the method name " 135 | "check() on models, model fields and model managers. You " 136 | "will need to rename any existing 'check()' methods.") 137 | return [], errors, [] 138 | 139 | 140 | def check_get_cache(line, filename, options): 141 | warnings = [] 142 | if 'django.core.cache' in line and 'get_cache' in line: 143 | warnings.append("django.core.cache.get_cache() is deprecated. Use " 144 | "django.core.cache.caches instead.") 145 | return warnings, [], [] 146 | 147 | 148 | def check_dictconfig_importlib(line, filename, options): 149 | warnings = [] 150 | if 'django.utils' in line and \ 151 | ('dictconfig' in line or 'importlib' in line): 152 | warnings.append("django.utils.dictconfig and django.utils.importlib " 153 | "are deprecated. Use logging.config or importlib from " 154 | "the Python standard library instead.") 155 | return warnings, [], [] 156 | 157 | 158 | def check_import_by_path(line, filename, options): 159 | warnings = [] 160 | if ('django.utils.module_loading' in line and 161 | 'import_by_path' in line) or \ 162 | 'import_by_path(' in line: 163 | warnings.append("django.utils.module_loading.import_by_path is " 164 | "deprecated. Use import_string() from the same module " 165 | "instead.") 166 | return warnings, [], [] 167 | 168 | 169 | def check_tzinfo(line, filename, options): 170 | warnings = [] 171 | if 'LocalTimezone' in line or 'FixedOffset' in line: 172 | warnings.append("django.utils.tzinfo.LocalTimezone and " 173 | "django.utils.tzinfo.FixedOffset are deprecated. Use " 174 | "django.utils.timezone.get_default_timezone() or " 175 | "django.utils.timezone.get_fixed_timezone() instead.") 176 | return warnings, [], [] 177 | 178 | 179 | def check_unittest(line, filename, options): 180 | warnings = [] 181 | if 'django.utils' in line and 'unittest' in line: 182 | warnings.append("django.utils.unittest is deprecated. Use the unittest " 183 | "module in the Python standard library instead.") 184 | return warnings, [], [] 185 | 186 | 187 | def check_sorteddict(line, filename, options): 188 | warnings = [] 189 | if 'SortedDict' in line: 190 | warnings.append("django.utils.datastructures.SortedDict is deprecated. " 191 | "Use collections.OrderedDict from the Python standard " 192 | "library instead.") 193 | return warnings, [], [] 194 | 195 | 196 | def check_sites(line, filename, options): 197 | warnings = [] 198 | if 'RequestSite' in line or 'get_current_site(' in line: 199 | warnings.append("django.contrib.sites.RequestSite and " 200 | "django.contrib.sites.get_current_site() have been moved. " 201 | "Use django.contrib.sites.requests.RequestSite or " 202 | "django.contrib.sites.shortcuts.get_current_site() instead.") 203 | return warnings, [], [] 204 | 205 | 206 | def check_declared_fieldsets(line, filename, options): 207 | warnings = [] 208 | if 'declared_fieldsets' in line: 209 | warnings.append("ModelAdmin.declared_fieldsets is deprecated. Implement the " 210 | "get_fieldsets() method instead.") 211 | return warnings, [], [] 212 | 213 | 214 | def check_contenttypes(line, filename, options): 215 | warnings = [] 216 | moves = [('GenericForeignKey', 'fields'), 217 | ('GenericRelation', 'fields'), 218 | ('BaseGenericInlineFormSet', 'forms'), 219 | ('generic_inlineformset_factory', 'forms'), 220 | ('GenericInlineModelAdmin', 'admin'), 221 | ('GenericStackedInline', 'admin'), 222 | ('GenericTabularInline', 'admin')] 223 | for cls, new_location in moves: 224 | if cls in line: 225 | warnings.append("django.contrib.contenttypes.generic.%s is now located in " 226 | "django.contrib.contenttypes.%s" % (cls, new_location)) 227 | return warnings, [], [] 228 | 229 | 230 | def check_utils_modules(line, filename, options): 231 | warnings = [] 232 | module_locations = ('django.contrib.admin', 'django.contrib.gis.db.backends', 233 | 'django.db.backends', 'django.forms') 234 | for mod in module_locations: 235 | if mod in line and 'util' in line and 'utils' not in line: 236 | warnings.append("%s.util has been renamed to %s.utils" % (mod, mod)) 237 | return warnings, [], [] 238 | 239 | 240 | def check_get_formsets(line, filename, options): 241 | warnings = [] 242 | if 'admin' in filename and 'get_formsets' in line: 243 | warnings.append("ModelAdmin.get_formsets() has been renamed to " 244 | "ModelAdmin.get_formsets_with_inlines().") 245 | return warnings, [], [] 246 | 247 | 248 | def check_ipaddressfields(line, filename, options): 249 | warnings = [] 250 | if ('IPAddressField' in line and 251 | 'GenericIPAddressField' not in line): 252 | warnings.append("IPAddressField is deprecated in favor of " 253 | "GenericIPAddressField.") 254 | return warnings, [], [] 255 | 256 | 257 | def check_get_memcached_timeout(line, filename, options): 258 | warnings = [] 259 | if '_get_memcache_timeout(' in line: 260 | warnings.append("_get_memcache_timeout is deprecated in favor of " 261 | "get_backend_timeout().") 262 | return warnings, [], [] 263 | 264 | 265 | def check_request_request(line, filename, options): 266 | warnings = [] 267 | if 'request.REQUEST' in line: 268 | warnings.append("request.REQUEST is deprecated. Use request.GET or " 269 | "request.POST as appropriate.") 270 | return warnings, [], [] 271 | 272 | 273 | def check_mergedict(line, filename, options): 274 | warnings = [] 275 | if 'MergeDict' in line: 276 | warnings.append("django.utils.datastructures.MergeDict is deprecated. " 277 | "Use standard dict.update() calls instead.") 278 | return warnings, [], [] 279 | 280 | 281 | def check_memoize(line, filename, options): 282 | warnings = [] 283 | if 'django.utils.functional' in line and 'memoize' in line: 284 | warnings.append("django.utils.functional.memoize is deprecated. On " 285 | "Python 2, use django.utils.lru_cache.lru_cache. On " 286 | "Python 3, use functools.lru_cache.") 287 | return warnings, [], [] 288 | 289 | 290 | def check_admin_for(line, filename, options): 291 | warnings = [] 292 | if 'ADMIN_FOR' in line: 293 | warnings.append("The ADMIN_FOR setting is no longer used.") 294 | return warnings, [], [] 295 | 296 | 297 | def check_splitdatetimewidget(line, filename, options): 298 | warnings = [] 299 | if 'SplitDateTimeWidget' in line: 300 | warnings.append("Use of SplitDateTimeWidget with DateTimeField " 301 | "is deprecated. Use it with SplitDateTimeField " 302 | "instead.") 303 | return warnings, [], [] 304 | 305 | 306 | def check_requires_model_validation(line, filename, options): 307 | errors = [] 308 | if 'requires_model_validation' in line: 309 | errors.append("The 'requires_model_validation' option of " 310 | "management commands is deprecated. Use " 311 | "requires_system_checks instead. Use of " 312 | "both is an error.") 313 | return [], errors, [] 314 | 315 | 316 | def check_validate_field(line, filename, options): 317 | warnings = [] 318 | if 'def' in line and 'validate_field(' in line: 319 | warnings.append("DatabaseValidation.validate_field() is deprecated. " 320 | "Implement check_field() instead.") 321 | return warnings, [], [] 322 | 323 | 324 | def check_future_tags(line, filename, options): 325 | warnings = [] 326 | if 'from future %}' in line: 327 | warnings.append("{% load ssi from future %} and " 328 | "{% load url from future %} are " 329 | "deprecated and no longer necessary; simply " 330 | "remove the 'from future'.") 331 | return warnings, [], [] 332 | 333 | 334 | def check_javascript_quote(line, filename, options): 335 | errors = [] 336 | if 'javascript_quote' in line: 337 | errors.append("django.utils.text.javascript_quote() should " 338 | "not be used; use the escapejs template tag " 339 | "or django.utils.html.escapejs() instead.") 340 | return [], errors, [] 341 | 342 | 343 | def check_fix_ampersands(line, filename, options): 344 | warnings = [] 345 | if 'fix_ampersands' in line: 346 | warnings.append("The fix_ampersands template filter and " 347 | "django.utils.html.fix_ampersands() are " 348 | "deprecated; Django's template escaping " 349 | "performs this automatically.") 350 | return warnings, [], [] 351 | 352 | 353 | def check_itercompat_product(line, filename, options): 354 | errors = [] 355 | if 'itercompat' in line and 'product' in line: 356 | errors.append("django.utils.itercompat.product() has been " 357 | "removed.") 358 | return [], errors, [] 359 | 360 | 361 | def check_simplejson(line, filename, options): 362 | errors = [] 363 | if 'django.utils' in line and 'simplejson' in line: 364 | errors.append("django.utils.simplejson has been removed.") 365 | return [], errors, [] 366 | 367 | 368 | def check_mimetype(line, filename, options): 369 | errors = [] 370 | if 'mimetype' in line: 371 | errors.append("The 'mimetype' option for HTTP response classes " 372 | "and rendering shortcuts has been removed. Use " 373 | "'content_type' instead.") 374 | return [], errors, [] 375 | 376 | 377 | def check_old_profile_methods(line, filename, options): 378 | errors = [] 379 | if 'AUTH_PROFILE_MODULE' in line or \ 380 | 'get_profile(' in line: 381 | errors.append("The AUTH_PROFILE_MODULE setting and the " 382 | " User.get_profile() method have been removed.") 383 | return [], errors, [] 384 | 385 | 386 | def check_select_related_depth(line, filename, options): 387 | errors = [] 388 | if 'select_related' in line and 'depth' in line: 389 | errors.append("The 'depth' argument to select_related() has " 390 | "been removed.") 391 | return [], errors, [] 392 | 393 | 394 | def check_test_cookie(line, filename, options): 395 | errors = [] 396 | if 'check_for_test_cookie' in line: 397 | errors.append("The check_for_test_cookie() method of " 398 | "AuthenticationForm has been removed.") 399 | return [], errors, [] 400 | 401 | 402 | def check_strandunicode(line, filename, options): 403 | errors = [] 404 | if 'StrAndUnicode' in line: 405 | errors.append("django.utils.encoding.StrAndUnicode has been " 406 | "removed.") 407 | return [], errors, [] 408 | 409 | 410 | def do_check_django_17(line, filename, options): 411 | warnings = [] 412 | info = [] 413 | errors = [] 414 | 415 | for check in [globals()[f] for f in globals().keys() if 416 | callable(globals()[f]) and f.startswith('check_')]: 417 | warn, err, inf = check(line, filename, options) 418 | warnings += warn 419 | info += inf 420 | errors += err 421 | 422 | return warnings, errors, info 423 | 424 | 425 | rules = [ 426 | {'long_option': '--django-17', 427 | 'action': 'store_true', 428 | 'dest': 'django_17', 429 | 'help': 'Check for changes and deprecations in Django 1.7.', 430 | 'callback': do_check_django_17, 431 | 'enabled': lambda options: options.django_17,} 432 | ] 433 | -------------------------------------------------------------------------------- /rules/django_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | DJANGO_UTILS_PATTERN = re.compile(r'(from django\.utils import |import django\.utils\.|from django\.utils\.)(?P\w+)') 5 | 6 | 7 | SAFE_UTILS_MODULES = [ 8 | 'cache', 9 | 'encoding', 10 | 'feedgenerator', 11 | 'http', 12 | 'safestring', 13 | 'translation', 14 | 'tzinfo', 15 | ] 16 | 17 | DJANGO_14_UTILS = [ 18 | '_os', 19 | 'archive', 20 | 'autoreload', 21 | 'baseconv', 22 | 'cache', 23 | 'checksums', 24 | 'copycompat', 25 | 'crypto', 26 | 'daemonize', 27 | 'datastructures', 28 | 'dateformat', 29 | 'dateparse', 30 | 'dates', 31 | 'datetime_safe', 32 | 'decorators', 33 | 'dictconfig', 34 | 'encoding', 35 | 'feedgenerator', 36 | 'formats', 37 | 'functional', 38 | 'hashcompat', 39 | 'html', 40 | 'html_parser', 41 | 'http', 42 | 'importlib', 43 | 'ipv6', 44 | 'itercompat', 45 | 'jslex', 46 | 'log', 47 | 'module_loading', 48 | 'numberformat', 49 | 'regex_helper', 50 | 'safestring', 51 | 'simplejson', 52 | 'six', 53 | 'synch', 54 | 'termcolors', 55 | 'text', 56 | 'timesince', 57 | 'timezone', 58 | 'translation', 59 | 'tree', 60 | 'tzinfo', 61 | 'unittest', 62 | 'version', 63 | 'xmlutils', 64 | ] 65 | 66 | UTILS_STDLIB_MAP = { 67 | 'functional': 'functools', 68 | 'simplejson': 'json', 69 | '_threading_local': 'threading', 70 | 'thread_support': 'threading', 71 | } 72 | 73 | 74 | def check_utils(line, filename, options): 75 | warnings = [] 76 | errors = [] 77 | info = [] 78 | module = None 79 | django_utils = DJANGO_UTILS_PATTERN.search(line) 80 | if django_utils: 81 | module = django_utils.groupdict()['module'] 82 | if module in SAFE_UTILS_MODULES: 83 | return warnings, errors, info 84 | if module not in DJANGO_14_UTILS: 85 | errors.append( 86 | 'django.utils.%s does not exist in Django 1.4.' % module) 87 | if module in UTILS_STDLIB_MAP: 88 | info.append( 89 | 'Use %s in stdlib instead of django.utils.%s.' % ( 90 | UTILS_STDLIB_MAP[module], module)) 91 | if module != 'datastructures' and 'SortedDict' not in line: 92 | warnings.append( 93 | 'django.utils.%s is not considered stable public API.' % module) 94 | return warnings, errors, info 95 | 96 | 97 | rules = [ 98 | {'long_option': '--utils', 99 | 'action': 'store_true', 100 | 'dest': 'utils', 101 | 'help': 'Check for unsafe use of django.utils.', 102 | 'callback': check_utils, 103 | 'enabled': lambda options: options.utils,} 104 | ] 105 | --------------------------------------------------------------------------------