diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 25fe24d2b..3abc67892 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -14,29 +14,33 @@ jobs: max-parallel: 4 matrix: python-version: - - 3.6 - 3.7 - 3.8 - 3.9 - "3.10" - - pypy3 + - "pypy3.9" tox-django-version: - "32" - "40" + - "41" # GH Actions don't support something like allow-failure ? # - "master" exclude: - - python-version: "3.6" - tox-django-version: "40" - python-version: "3.7" tox-django-version: "40" - - python-version: "pypy3" + - python-version: "pypy3.9" tox-django-version: "40" + - python-version: "3.6" + tox-django-version: "41" + - python-version: "3.7" + tox-django-version: "41" + - python-version: "pypy3.9" + tox-django-version: "41" steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - run: python -m pip install tox @@ -69,9 +73,9 @@ jobs: - "32" steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - run: python -m pip install tox diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4ec55c1..71437a3d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ Changelog See https://github.com/django-extensions/django-extensions/releases +3.2.1 +----- + +Changes: + +- Improvement: fix translation interpolation in prospective arabic translations (#1740) +- Improvement: runserver_plus, Add option to ignore files on runserver_plus reload (#1762) +- Improvement: docs: Fix a few typos (#1764) (#1751) +- Improvement: drop python 3.5 as it is EOL (#1735) +- Improvement: sqldiff, Added support for meta indexes and constraints in sqldiff. (#1726) +- Improvement: show_urls, Ensure consistent output in show_urls for django 4.0+ (#1759) +- Fix: dumpscript, make_aware should not be called if aware already (#1745) +- Fix: Use list values for requires_system_checks (#1736) + 3.2.0 ----- @@ -18,7 +32,6 @@ Changes: - Fix: runserver_plus, Fix KeyError: 'werkzeug.server.shutdown' - New: managestate, Saves current applied migrations to a file or applies migrations from file - 3.1.5 ----- diff --git a/README.rst b/README.rst index 431cc1efe..36d5f866e 100644 --- a/README.rst +++ b/README.rst @@ -111,7 +111,7 @@ translation in your language. If you have some time to spare and like to help us - GitHub: https://github.com/django-extensions/django-extensions - Mailing list: http://groups.google.com/group/django-extensions -- Translations: https://www.transifex.net/projects/p/django-extensions/ +- Translations: https://www.transifex.com/projects/p/django-extensions/ Documentation diff --git a/django_extensions/__init__.py b/django_extensions/__init__.py index 2e5702486..8f4c64ef1 100644 --- a/django_extensions/__init__.py +++ b/django_extensions/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -VERSION = (3, 2, 0) +VERSION = (3, 2, 1) def get_version(version): @@ -18,9 +18,3 @@ def get_version(version): __version__ = get_version(VERSION) - -try: - default_app_config = 'django_extensions.apps.DjangoExtensionsConfig' -except ModuleNotFoundError: - # this part is useful for allow setup.py to be used for version checks - pass diff --git a/django_extensions/locale/ar/LC_MESSAGES/django.po b/django_extensions/locale/ar/LC_MESSAGES/django.po index 0963b4f8c..89875dcb0 100644 --- a/django_extensions/locale/ar/LC_MESSAGES/django.po +++ b/django_extensions/locale/ar/LC_MESSAGES/django.po @@ -22,7 +22,7 @@ msgstr "و" #: admin/__init__.py:141 #, python-format msgid "Use the left field to do %(model_name)s lookups in the fields %(field_list)s." -msgstr "إستعمل الحقل الأيسر من (model_name)% لبحث ضمن الأحقال التالية (field_list)% " +msgstr "إستعمل الحقل الأيسر من %(model_name)s لبحث ضمن الأحقال التالية %(field_list)s " #: admin/filter.py:24 admin/filter.py:53 msgid "Yes" @@ -79,7 +79,7 @@ msgstr "أترك الحقل فارغ لتنشيط لمدة غير محددة" #: mongodb/fields/__init__.py:22 #, python-format msgid "String (up to %(max_length)s)" -msgstr "سلسلة الإحرف (طولها يصل إلى %(max_length))" +msgstr "سلسلة الإحرف (طولها يصل إلى %(max_length)s)" #: validators.py:14 msgid "Control Characters like new lines or tabs are not allowed." @@ -101,9 +101,9 @@ msgstr "الطول غير مقبول, يجب أن لا يكون أطول من %( #: validators.py:76 #, python-format msgid "Ensure that there are more than %(min)s characters." -msgstr "تأكد أن طول سلسلة الإحرف أطول من (min)% " +msgstr "تأكد أن طول سلسلة الإحرف أطول من %(min)s " #: validators.py:77 #, python-format msgid "Ensure that there are no more than %(max)s characters." -msgstr "تأكد أن طول سلسلة الأحرف لا تتجوز (max)% " +msgstr "تأكد أن طول سلسلة الأحرف لا تتجوز %(max)s " diff --git a/django_extensions/management/commands/clean_pyc.py b/django_extensions/management/commands/clean_pyc.py index f710eb511..a3197a9d5 100644 --- a/django_extensions/management/commands/clean_pyc.py +++ b/django_extensions/management/commands/clean_pyc.py @@ -2,6 +2,7 @@ import fnmatch import os from os.path import join as _j +from typing import List from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -12,7 +13,7 @@ class Command(BaseCommand): help = "Removes all python bytecode compiled files from the project." - requires_system_checks = False + requires_system_checks: List[str] = [] def add_arguments(self, parser): parser.add_argument( diff --git a/django_extensions/management/commands/compile_pyc.py b/django_extensions/management/commands/compile_pyc.py index 1b702fd7b..0bcd1b466 100644 --- a/django_extensions/management/commands/compile_pyc.py +++ b/django_extensions/management/commands/compile_pyc.py @@ -3,6 +3,7 @@ import os import py_compile from os.path import join as _j +from typing import List from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -12,7 +13,7 @@ class Command(BaseCommand): help = "Compile python bytecode files for the project." - requires_system_checks = False + requires_system_checks: List[str] = [] def add_arguments(self, parser): parser.add_argument('--path', '-p', action='store', dest='path', diff --git a/django_extensions/management/commands/create_command.py b/django_extensions/management/commands/create_command.py index 5844f91e3..d48c8dd99 100644 --- a/django_extensions/management/commands/create_command.py +++ b/django_extensions/management/commands/create_command.py @@ -2,6 +2,7 @@ import os import sys import shutil +from typing import List from django.core.management.base import AppCommand from django.core.management.color import color_style @@ -12,7 +13,7 @@ class Command(AppCommand): help = "Creates a Django management command directory structure for the given app name in the app's directory." - requires_system_checks = False + requires_system_checks: List[str] = [] # Can't import settings during this command, because they haven't # necessarily been created. can_import_settings = True diff --git a/django_extensions/management/commands/create_jobs.py b/django_extensions/management/commands/create_jobs.py index 30c7e0cca..aa9aff1e9 100644 --- a/django_extensions/management/commands/create_jobs.py +++ b/django_extensions/management/commands/create_jobs.py @@ -2,6 +2,7 @@ import os import sys import shutil +from typing import List from django.core.management.base import AppCommand from django.core.management.color import color_style @@ -12,7 +13,7 @@ class Command(AppCommand): help = "Creates a Django jobs command directory structure for the given app name in the current directory." - requires_system_checks = False + requires_system_checks: List[str] = [] # Can't import settings during this command, because they haven't # necessarily been created. can_import_settings = True diff --git a/django_extensions/management/commands/create_template_tags.py b/django_extensions/management/commands/create_template_tags.py index c444e43f0..83ab9cf93 100644 --- a/django_extensions/management/commands/create_template_tags.py +++ b/django_extensions/management/commands/create_template_tags.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os import sys +from typing import List from django.core.management.base import AppCommand @@ -21,7 +22,7 @@ def add_arguments(self, parser): help='The name to use for the template tag base name. Defaults to `appname`_tags.' ) - requires_system_checks = False + requires_system_checks: List[str] = [] # Can't import settings during this command, because they haven't # necessarily been created. can_import_settings = True diff --git a/django_extensions/management/commands/dumpscript.py b/django_extensions/management/commands/dumpscript.py index 98392f4c5..d31ddcbd7 100644 --- a/django_extensions/management/commands/dumpscript.py +++ b/django_extensions/management/commands/dumpscript.py @@ -70,7 +70,8 @@ def orm_item_locator(orm_obj): v = clean_dict[key] if v is not None: if isinstance(v, datetime.datetime): - v = timezone.make_aware(v) + if not timezone.is_aware(v): + v = timezone.make_aware(v) clean_dict[key] = StrToCodeChanger('dateutil.parser.parse("%s")' % v.isoformat()) elif not isinstance(v, (str, int, float)): clean_dict[key] = str("%s" % v) diff --git a/django_extensions/management/commands/generate_password.py b/django_extensions/management/commands/generate_password.py index e52369484..071cbb27c 100644 --- a/django_extensions/management/commands/generate_password.py +++ b/django_extensions/management/commands/generate_password.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from typing import List + try: from django.contrib.auth.base_user import BaseUserManager except ImportError: @@ -10,7 +12,7 @@ class Command(BaseCommand): help = "Generates a new password that can be used for a user password. This uses Django core's default password generator `BaseUserManager.make_random_password()`." - requires_system_checks = False + requires_system_checks: List[str] = [] def add_arguments(self, parser): parser.add_argument( diff --git a/django_extensions/management/commands/generate_secret_key.py b/django_extensions/management/commands/generate_secret_key.py index f48232440..0ec129118 100644 --- a/django_extensions/management/commands/generate_secret_key.py +++ b/django_extensions/management/commands/generate_secret_key.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from typing import List + from django.core.management.base import BaseCommand from django.core.management.utils import get_random_secret_key @@ -8,7 +10,7 @@ class Command(BaseCommand): help = "Generates a new SECRET_KEY that can be used in a project settings file." - requires_system_checks = False + requires_system_checks: List[str] = [] @signalcommand def handle(self, *args, **options): diff --git a/django_extensions/management/commands/mail_debug.py b/django_extensions/management/commands/mail_debug.py index 4bf6b0ba6..7a8424dc4 100644 --- a/django_extensions/management/commands/mail_debug.py +++ b/django_extensions/management/commands/mail_debug.py @@ -3,6 +3,7 @@ import sys from logging import getLogger from smtpd import SMTPServer +from typing import List from django.core.management.base import BaseCommand, CommandError @@ -33,7 +34,7 @@ class Command(BaseCommand): help = "Starts a test mail server for development." args = '[optional port number or ippaddr:port]' - requires_system_checks = False + requires_system_checks: List[str] = [] def add_arguments(self, parser): super().add_arguments(parser) diff --git a/django_extensions/management/commands/runserver_plus.py b/django_extensions/management/commands/runserver_plus.py index 867e85fb4..e8f104c58 100644 --- a/django_extensions/management/commands/runserver_plus.py +++ b/django_extensions/management/commands/runserver_plus.py @@ -7,7 +7,7 @@ import traceback import webbrowser import functools -from typing import Set +from typing import List, Set import django from django.conf import settings @@ -128,7 +128,7 @@ class Command(BaseCommand): help = "Starts a lightweight Web server for development." # Validation is called explicitly each time the server is reloaded. - requires_system_checks = False + requires_system_checks: List[str] = [] DEFAULT_CRT_EXTENSION = ".crt" DEFAULT_KEY_EXTENSION = ".key" @@ -165,6 +165,8 @@ def add_arguments(self, parser): 'Either --cert-file or --key-file must be provided to use SSL.') parser.add_argument('--extra-file', dest='extra_files', action="append", type=str, default=[], help='auto-reload whenever the given file changes too (can be specified multiple times)') + parser.add_argument('--exclude-pattern', dest='exclude_patterns', action="append", type=str, default=[], + help='ignore reload on changes to files matching this pattern (can be specified multiple times)') parser.add_argument('--reloader-interval', dest='reloader_interval', action="store", type=int, default=DEFAULT_POLLER_RELOADER_INTERVAL, help='After how many seconds auto-reload should scan for updates in poller-mode [default=%s]' % DEFAULT_POLLER_RELOADER_INTERVAL) parser.add_argument('--reloader-type', dest='reloader_type', action="store", type=str, default=DEFAULT_POLLER_RELOADER_TYPE, @@ -333,6 +335,7 @@ def make_environ(self): reloader_interval = options['reloader_interval'] reloader_type = options['reloader_type'] self.extra_files = set(options['extra_files']) + exclude_patterns = set(options['exclude_patterns']) self.nopin = options['nopin'] @@ -392,6 +395,8 @@ def make_environ(self): if getattr(settings, 'RUNSERVER_PLUS_EXTRA_FILES', []): self.extra_files |= set(settings.RUNSERVER_PLUS_EXTRA_FILES) + exclude_patterns |= set(getattr(settings, 'RUNSERVER_PLUS_EXCLUDE_PATTERNS', [])) + # Werkzeug needs to be clued in its the main instance if running # without reloader or else it won't show key. # https://git.io/vVIgo @@ -413,6 +418,7 @@ def make_environ(self): use_reloader=use_reloader, use_debugger=True, extra_files=self.extra_files, + exclude_patterns=exclude_patterns, reloader_interval=reloader_interval, reloader_type=reloader_type, threaded=threaded, diff --git a/django_extensions/management/commands/set_fake_emails.py b/django_extensions/management/commands/set_fake_emails.py index 4eb4a9c8b..4a5e34362 100644 --- a/django_extensions/management/commands/set_fake_emails.py +++ b/django_extensions/management/commands/set_fake_emails.py @@ -8,6 +8,8 @@ """ +from typing import List + from django.conf import settings from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError @@ -19,7 +21,7 @@ class Command(BaseCommand): help = '''DEBUG only: give all users a new email based on their account data ("%s" by default). Possible parameters are: username, first_name, last_name''' % (DEFAULT_FAKE_EMAIL, ) - requires_system_checks = False + requires_system_checks: List[str] = [] def add_arguments(self, parser): super().add_arguments(parser) diff --git a/django_extensions/management/commands/set_fake_passwords.py b/django_extensions/management/commands/set_fake_passwords.py index 5d619bd02..1d03fe037 100644 --- a/django_extensions/management/commands/set_fake_passwords.py +++ b/django_extensions/management/commands/set_fake_passwords.py @@ -7,6 +7,8 @@ setting.DEBUG is True. """ +from typing import List + from django.conf import settings from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError @@ -18,7 +20,7 @@ class Command(BaseCommand): help = 'DEBUG only: sets all user passwords to a common value ("%s" by default)' % (DEFAULT_FAKE_PASSWORD, ) - requires_system_checks = False + requires_system_checks: List[str] = [] def add_arguments(self, parser): super().add_arguments(parser) diff --git a/django_extensions/management/commands/show_urls.py b/django_extensions/management/commands/show_urls.py index cddca120e..9f60023ba 100644 --- a/django_extensions/management/commands/show_urls.py +++ b/django_extensions/management/commands/show_urls.py @@ -125,6 +125,8 @@ def handle(self, *args, **options): func = func.func decorators.insert(0, 'functools.partial') + if hasattr(func, 'view_class'): + func = func.view_class if hasattr(func, '__name__'): func_name = func.__name__ elif hasattr(func, '__class__'): diff --git a/django_extensions/management/commands/sqlcreate.py b/django_extensions/management/commands/sqlcreate.py index a0af17549..9a579af35 100644 --- a/django_extensions/management/commands/sqlcreate.py +++ b/django_extensions/management/commands/sqlcreate.py @@ -2,6 +2,7 @@ import socket import sys import warnings +from typing import List from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -19,7 +20,7 @@ class Command(BaseCommand): ./manage.py sqlcreate [--database=] | mysql -u -p ./manage.py sqlcreate [--database=] | psql -U -W""" - requires_system_checks = False + requires_system_checks: List[str] = [] can_import_settings = True def add_arguments(self, parser): diff --git a/django_extensions/management/commands/sqldiff.py b/django_extensions/management/commands/sqldiff.py index 550ed8f7b..20849dadb 100644 --- a/django_extensions/management/commands/sqldiff.py +++ b/django_extensions/management/commands/sqldiff.py @@ -30,6 +30,7 @@ from django.core.management.base import OutputWrapper from django.core.management.color import no_style from django.db import connection, transaction, models +from django.db.models import UniqueConstraint from django.db.models.fields import AutoField, IntegerField from django.db.models.options import normalize_together @@ -381,6 +382,23 @@ def strip_parameters(self, field_type): return field_type.split(" ")[0].split("(")[0].lower() return field_type + def get_index_together(self, meta): + indexes_normalized = list(normalize_together(meta.index_together)) + + for idx in meta.indexes: + indexes_normalized.append(idx.fields) + + return self.expand_together(indexes_normalized, meta) + + def get_unique_together(self, meta): + unique_normalized = list(normalize_together(meta.unique_together)) + + for constraint in meta.constraints: + if isinstance(constraint, UniqueConstraint): + unique_normalized.append(constraint.fields) + + return self.expand_together(unique_normalized, meta) + def expand_together(self, together, meta): new_together = [] for fields in normalize_together(together): @@ -411,7 +429,7 @@ def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, tabl if db_type.startswith('text'): self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' text_pattern_ops') - unique_together = self.expand_together(meta.unique_together, meta) + unique_together = self.get_unique_together(meta) db_unique_columns = normalize_together([v['columns'] for v in table_constraints.values() if v['unique'] and not v['index']]) for unique_columns in unique_together: @@ -427,7 +445,7 @@ def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, tabl def find_unique_missing_in_model(self, meta, table_indexes, table_constraints, table_name): fields = dict([(field.column, field) for field in all_local_fields(meta)]) - unique_together = self.expand_together(meta.unique_together, meta) + unique_together = self.get_unique_together(meta) for constraint_name, constraint in table_constraints.items(): if not constraint['unique']: @@ -463,7 +481,7 @@ def find_index_missing_in_db(self, meta, table_indexes, table_constraints, table if db_type.startswith('text'): self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' text_pattern_ops') - index_together = self.expand_together(meta.index_together, meta) + index_together = self.get_index_together(meta) db_index_together = normalize_together([v['columns'] for v in table_constraints.values() if v['index'] and not v['unique']]) for columns in index_together: if columns in db_index_together: @@ -478,7 +496,7 @@ def find_index_missing_in_db(self, meta, table_indexes, table_constraints, table def find_index_missing_in_model(self, meta, table_indexes, table_constraints, table_name): fields = dict([(field.column, field) for field in all_local_fields(meta)]) meta_index_names = [idx.name for idx in meta.indexes] - index_together = self.expand_together(meta.index_together, meta) + index_together = self.get_index_together(meta) for constraint_name, constraint in table_constraints.items(): if constraint_name in meta_index_names: @@ -642,7 +660,7 @@ def find_differences(self): transaction.rollback() # reset transaction continue - # map table_contraints into table_indexes + # map table_constraints into table_indexes table_indexes = {} for contraint_name, dct in table_constraints.items(): @@ -838,8 +856,8 @@ def get_field_db_type(self, description, field=None, table_name=None): def find_index_missing_in_model(self, meta, table_indexes, table_constraints, table_name): fields = dict([(field.column, field) for field in all_local_fields(meta)]) meta_index_names = [idx.name for idx in meta.indexes] - index_together = self.expand_together(meta.index_together, meta) - unique_together = self.expand_together(meta.unique_together, meta) + index_together = self.get_index_together(meta) + unique_together = self.get_unique_together(meta) for constraint_name, constraint in table_constraints.items(): if constraint_name in meta_index_names: @@ -904,7 +922,7 @@ def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, tabl if db_type.startswith('text'): self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' text_pattern_ops') - unique_together = self.expand_together(meta.unique_together, meta) + unique_together = self.get_unique_together(meta) # This comparison changed from superclass - otherwise function is the same db_unique_columns = normalize_together([v['columns'] for v in table_constraints.values() if v['unique']]) @@ -953,7 +971,7 @@ def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, tabl if column in unique_columns and (constraint['unique'] or constraint['primary_key']): skip_list.append(column) - unique_together = self.expand_together(meta.unique_together, meta) + unique_together = self.get_unique_together(meta) db_unique_columns = normalize_together([v['columns'] for v in table_constraints.values() if v['unique']]) for unique_columns in unique_together: @@ -1192,7 +1210,7 @@ def get_field_db_type(self, description, field=None, table_name=None): # constraints for the type in `get_data_type_arrayfield` which instantiates # the array base_field or maybe even better restructure sqldiff entirely # to be based around the concrete type yielded by the code below. That gives - # the complete type the database uses, why not use thie much earlier in the + # the complete type the database uses, why not use this much earlier in the # process to compare to whatever django spits out as the desired database type ? attname = field.db_column or field.attname introspect_db_type = self.sql_to_dict( diff --git a/django_extensions/management/commands/sqldsn.py b/django_extensions/management/commands/sqldsn.py index 32d732fbb..e5795205d 100644 --- a/django_extensions/management/commands/sqldsn.py +++ b/django_extensions/management/commands/sqldsn.py @@ -7,6 +7,7 @@ import sys import warnings +from typing import List from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -84,7 +85,7 @@ def inner(dbhost, dbport, dbname, dbuser, dbpass): class Command(BaseCommand): help = "Prints DSN on stdout, as specified in settings.py" - requires_system_checks = False + requires_system_checks: List[str] = [] can_import_settings = True def add_arguments(self, parser): diff --git a/django_extensions/mongodb/fields/__init__.py b/django_extensions/mongodb/fields/__init__.py index 857a5dba0..ee28484d9 100644 --- a/django_extensions/mongodb/fields/__init__.py +++ b/django_extensions/mongodb/fields/__init__.py @@ -125,7 +125,7 @@ def create_slug(self, model_instance, add): if model_instance.pk: queryset = queryset.exclude(pk=model_instance.pk) - # form a kwarg dict used to impliment any unique_together contraints + # form a kwarg dict used to impliment any unique_together constraints kwargs = {} for params in model_instance._meta.unique_together: if self.attname in params: diff --git a/django_extensions/templatetags/syntax_color.py b/django_extensions/templatetags/syntax_color.py index 986b5a91a..de72016e6 100644 --- a/django_extensions/templatetags/syntax_color.py +++ b/django_extensions/templatetags/syntax_color.py @@ -21,7 +21,7 @@ {{ code_string|colorize }} -You may also render the syntax highlighed text with line numbers. +You may also render the syntax highlighted text with line numbers. {% load syntax_color %} {{ some_code|colorize_table:"html+django" }} diff --git a/docs/command_extensions.rst b/docs/command_extensions.rst index ea8299ded..2a4d6fee2 100644 --- a/docs/command_extensions.rst +++ b/docs/command_extensions.rst @@ -16,6 +16,7 @@ Command Extensions graph_models list_model_info list_signals + managestate merge_model_instances print_settings reset_db diff --git a/docs/conf.py b/docs/conf.py index 78a91697b..442c1986e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,7 +47,7 @@ # The short X.Y version. version = '3.2' # The full version, including alpha/beta/rc tags. -release = '3.2.0' +release = '3.2.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/runserver_plus.rst b/docs/runserver_plus.rst index 9b8e051b8..11a24c13e 100644 --- a/docs/runserver_plus.rst +++ b/docs/runserver_plus.rst @@ -206,6 +206,9 @@ Other configuration options and their defaults include: # Add extra files to watch RUNSERVER_PLUS_EXTRA_FILES = [] + # Do not watch files matching any of these patterns + RUNSERVER_PLUS_EXCLUDE_PATTERNS = [] + IO Calls and CPU Usage ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/validate_templates.rst b/docs/validate_templates.rst index 9bbf83c60..956d1b3e0 100644 --- a/docs/validate_templates.rst +++ b/docs/validate_templates.rst @@ -27,7 +27,7 @@ break ~~~~~ Do not continue scanning other templates after the first failure. -ignore_app +ignore-app ~~~~~~~~~~ Ignore this app (can be used multiple times). @@ -35,7 +35,7 @@ includes ~~~~~~~~ Use -i (can be used multiple times) to add directories to the TEMPLATE DIRS. -no_apps +no-apps ~~~~~~~ Do not automatically include app template directories. diff --git a/setup.py b/setup.py index bcbc1df7d..6a2232f8d 100644 --- a/setup.py +++ b/setup.py @@ -115,6 +115,7 @@ def fullsplit(path, result=None): 'Framework :: Django', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4.0', + 'Framework :: Django :: 4.1', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', diff --git a/tests/management/commands/test_show_urls.py b/tests/management/commands/test_show_urls.py index aed4287b4..1b14abb8f 100644 --- a/tests/management/commands/test_show_urls.py +++ b/tests/management/commands/test_show_urls.py @@ -7,7 +7,6 @@ from django.test import TestCase from django.test.utils import override_settings from django.views.generic.base import View -from django import VERSION from unittest.mock import Mock, patch @@ -58,23 +57,14 @@ def test_should_show_urls_unsorted_but_same_order_as_found_in_url_patterns(self, lines = m_stdout.getvalue().splitlines() self.assertIn('/lambda/view\ttests.management.commands.test_show_urls.', lines[0]) self.assertIn('/function/based/\ttests.management.commands.test_show_urls.function_based_view\tfunction-based-view', lines[1]) - - if VERSION >= (4, 0): - self.assertIn('/class/based/\ttests.management.commands.test_show_urls.view\tclass-based-view', lines[2]) - else: - self.assertIn('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[2]) + self.assertIn('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[2]) @patch('sys.stdout', new_callable=StringIO) def test_should_show_urls_sorted_alphabetically(self, m_stdout): call_command('show_urls', verbosity=3) lines = m_stdout.getvalue().splitlines() - - if VERSION >= (4, 0): - self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.view\tclass-based-view', lines[0]) - else: - self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[0]) - + self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[0]) self.assertEqual('/function/based/\ttests.management.commands.test_show_urls.function_based_view\tfunction-based-view', lines[1]) self.assertEqual('/lambda/view\ttests.management.commands.test_show_urls.', lines[2]) @@ -82,45 +72,29 @@ def test_should_show_urls_sorted_alphabetically(self, m_stdout): def test_should_show_urls_in_json_format(self, m_stdout): call_command('show_urls', '--format=json') - json = [ + self.assertJSONEqual(m_stdout.getvalue(), [ {"url": "/lambda/view", "module": "tests.management.commands.test_show_urls.", "name": "", "decorators": ""}, - {"url": "/function/based/", "module": "tests.management.commands.test_show_urls.function_based_view", "name": "function-based-view", "decorators": ""} - ] - - if VERSION >= (4, 0): - json.append({"url": "/class/based/", "module": "tests.management.commands.test_show_urls.view", "name": "class-based-view", "decorators": ""}) - else: - json.append({"url": "/class/based/", "module": "tests.management.commands.test_show_urls.ClassView", "name": "class-based-view", "decorators": ""}) - - self.assertJSONEqual(m_stdout.getvalue(), json) + {"url": "/function/based/", "module": "tests.management.commands.test_show_urls.function_based_view", "name": "function-based-view", "decorators": ""}, + {"url": "/class/based/", "module": "tests.management.commands.test_show_urls.ClassView", "name": "class-based-view", "decorators": ""} + ]) self.assertEqual(len(m_stdout.getvalue().splitlines()), 1) @patch('sys.stdout', new_callable=StringIO) def test_should_show_urls_in_pretty_json_format(self, m_stdout): call_command('show_urls', '--format=pretty-json') - json = [ + self.assertJSONEqual(m_stdout.getvalue(), [ {"url": "/lambda/view", "module": "tests.management.commands.test_show_urls.", "name": "", "decorators": ""}, - {"url": "/function/based/", "module": "tests.management.commands.test_show_urls.function_based_view", "name": "function-based-view", "decorators": ""} - ] - - if VERSION >= (4, 0): - json.append({"url": "/class/based/", "module": "tests.management.commands.test_show_urls.view", "name": "class-based-view", "decorators": ""}) - else: - json.append({"url": "/class/based/", "module": "tests.management.commands.test_show_urls.ClassView", "name": "class-based-view", "decorators": ""}) - - self.assertJSONEqual(m_stdout.getvalue(), json) + {"url": "/function/based/", "module": "tests.management.commands.test_show_urls.function_based_view", "name": "function-based-view", "decorators": ""}, + {"url": "/class/based/", "module": "tests.management.commands.test_show_urls.ClassView", "name": "class-based-view", "decorators": ""} + ]) self.assertEqual(len(m_stdout.getvalue().splitlines()), 20) @patch('sys.stdout', new_callable=StringIO) def test_should_show_urls_in_table_format(self, m_stdout): call_command('show_urls', '--format=table') - if VERSION >= (4, 0): - self.assertIn('/class/based/ | tests.management.commands.test_show_urls.view | class-based-view |', m_stdout.getvalue()) - else: - self.assertIn('/class/based/ | tests.management.commands.test_show_urls.ClassView | class-based-view |', m_stdout.getvalue()) - + self.assertIn('/class/based/ | tests.management.commands.test_show_urls.ClassView | class-based-view |', m_stdout.getvalue()) self.assertIn('/function/based/ | tests.management.commands.test_show_urls.function_based_view | function-based-view |', m_stdout.getvalue()) self.assertIn('/lambda/view | tests.management.commands.test_show_urls. | |', m_stdout.getvalue()) @@ -129,12 +103,7 @@ def test_should_show_urls_in_aligned_format(self, m_stdout): call_command('show_urls', '--format=aligned') lines = m_stdout.getvalue().splitlines() - - if VERSION >= (4, 0): - self.assertEqual('/class/based/ tests.management.commands.test_show_urls.view class-based-view ', lines[0]) - else: - self.assertEqual('/class/based/ tests.management.commands.test_show_urls.ClassView class-based-view ', lines[0]) - + self.assertEqual('/class/based/ tests.management.commands.test_show_urls.ClassView class-based-view ', lines[0]) self.assertEqual('/function/based/ tests.management.commands.test_show_urls.function_based_view function-based-view ', lines[1]) self.assertEqual('/lambda/view tests.management.commands.test_show_urls. ', lines[2]) @@ -143,11 +112,6 @@ def test_should_show_urls_with_no_color_option(self, m_stdout): call_command('show_urls', '--no-color') lines = m_stdout.getvalue().splitlines() - - if VERSION >= (4, 0): - self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.view\tclass-based-view', lines[0]) - else: - self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[0]) - + self.assertEqual('/class/based/\ttests.management.commands.test_show_urls.ClassView\tclass-based-view', lines[0]) self.assertEqual('/function/based/\ttests.management.commands.test_show_urls.function_based_view\tfunction-based-view', lines[1]) self.assertEqual('/lambda/view\ttests.management.commands.test_show_urls.', lines[2]) diff --git a/tests/test_admin_filter.py b/tests/test_admin_filter.py index 9f2998819..27d22ae3d 100644 --- a/tests/test_admin_filter.py +++ b/tests/test_admin_filter.py @@ -35,7 +35,7 @@ def test_should_not_filter_qs_if_all_lookup_selected(self): result = filter_spec.queryset(self.request, self.qs) - self.assertQuerysetEqual(self.qs, map(repr, result), ordered=False) + self.assertCountEqual(self.qs, result) def test_should_return_objects_with_empty_text_if_yes_lookup_selected(self): expected_result = Secret.objects.filter(text__isnull=True) @@ -44,7 +44,7 @@ def test_should_return_objects_with_empty_text_if_yes_lookup_selected(self): result = filter_spec.queryset(self.request, self.qs) - self.assertQuerysetEqual(expected_result, map(repr, result), ordered=False) + self.assertCountEqual(expected_result, result) def test_should_return_objects_with_not_empty_text_value_if_no_lookup_selected(self): expected_result = Secret.objects.filter(text__isnull=False) @@ -53,7 +53,7 @@ def test_should_return_objects_with_not_empty_text_value_if_no_lookup_selected(s result = filter_spec.queryset(self.request, self.qs) - self.assertQuerysetEqual(expected_result, map(repr, result), ordered=False) + self.assertCountEqual(expected_result, result) def test_choices(self): expected_result = [ @@ -79,4 +79,4 @@ def test_should_not_filter_qs_if_all_lookup_selected(self): result = filter_spec.queryset(self.request, self.qs) - self.assertQuerysetEqual(self.qs, map(repr, result), ordered=False) + self.assertCountEqual(self.qs, result) diff --git a/tests/test_sqldiff.py b/tests/test_sqldiff.py index 24789c5c9..bd7faf712 100644 --- a/tests/test_sqldiff.py +++ b/tests/test_sqldiff.py @@ -9,6 +9,8 @@ # from django.core.management import call_command from django_extensions.management.commands.sqldiff import SqliteSQLDiff, Command, MySQLDiff, PostgresqlSQLDiff +from tests.testapp.models import PostWithUniqField, SluggedWithUniqueTogetherTestModel, \ + RandomCharTestModelUniqueTogether, SqlDiffUniqueTogether, SqlDiff, SqlDiffIndexes class SqlDiffTests(TestCase): @@ -53,6 +55,29 @@ def test_format_field_names(self): expected_field_name = ['name', 'email', 'address'] self.assertEqual(instance.format_field_names(['Name', 'EMAIL', 'aDDress']), expected_field_name) + def test_get_index_together(self): + instance = MySQLDiff( + apps.get_models(include_auto_created=True), + vars(self.options), + stdout=self.tmp_out, + stderr=self.tmp_err, + ) + self.assertEqual(instance.get_index_together(SqlDiff._meta), [('number', 'creator')]) + self.assertEqual(instance.get_index_together(SqlDiffIndexes._meta), [('first', 'second')]) + + def test_get_unique_together(self): + instance = MySQLDiff( + apps.get_models(include_auto_created=True), + vars(self.options), + stdout=self.tmp_out, + stderr=self.tmp_err, + ) + self.assertEqual(instance.get_unique_together(SluggedWithUniqueTogetherTestModel._meta), [('slug', 'category')]) + self.assertEqual(instance.get_unique_together(RandomCharTestModelUniqueTogether._meta), + [('random_char_field', 'common_field')]) + self.assertEqual(instance.get_unique_together(SqlDiffUniqueTogether._meta), [('aaa', 'bbb')]) + self.assertEqual(instance.get_unique_together(PostWithUniqField._meta), [('common_field', 'uniq_field')]) + @pytest.mark.skipif(settings.DATABASES['default']['ENGINE'] != 'django.db.backends.mysql', reason="Test can only run on mysql") def test_mysql_to_dict(self): mysql_instance = MySQLDiff( diff --git a/tests/testapp/__init__.py b/tests/testapp/__init__.py index e633c3b3f..e69de29bb 100644 --- a/tests/testapp/__init__.py +++ b/tests/testapp/__init__.py @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -default_app_config = 'tests.testapp.apps.TestAppConfig' diff --git a/tests/testapp/models.py b/tests/testapp/models.py index e044babcd..088b796d7 100644 --- a/tests/testapp/models.py +++ b/tests/testapp/models.py @@ -474,6 +474,19 @@ class SqlDiff(models.Model): number = models.CharField(max_length=40, null=True, verbose_name='Chargennummer') creator = models.CharField(max_length=20, null=True, blank=True) + class Meta: + index_together = ['number', 'creator'] + + +class SqlDiffIndexes(models.Model): + first = models.CharField(max_length=40, null=True, verbose_name='Chargennummer') + second = models.CharField(max_length=20, null=True, blank=True) + + class Meta: + indexes = [ + models.Index(fields=['first', 'second']), + ] + class SqlDiffUniqueTogether(models.Model): aaa = models.CharField(max_length=20) diff --git a/tests/testapp_with_appconfig/__init__.py b/tests/testapp_with_appconfig/__init__.py index ff08595b2..e69de29bb 100644 --- a/tests/testapp_with_appconfig/__init__.py +++ b/tests/testapp_with_appconfig/__init__.py @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -default_app_config = 'tests.testapp.apps.TestappWithAppConfigConfig' diff --git a/tox.ini b/tox.ini index 57ede1692..51c777bf5 100644 --- a/tox.ini +++ b/tox.ini @@ -11,11 +11,14 @@ envlist = {py37,py38,py39,py310}-flake8 {py36,py37,py38,py39,py310,pypy}-dj32 {py38,py39,py310,pypy}-dj40 + {py38,py39,py310,pypy}-dj41 {py38,py39,py310,pypy}-djmaster py310-dj32-postgres py310-dj40-postgres + py310-dj41-postgres py310-dj32-mysql py310-dj40-mysql + py310-dj41-mysql py310-djmaster-postgres [testenv] @@ -40,6 +43,7 @@ deps = -rrequirements-dev.txt dj32: Django>=3.2,<4.0 dj40: Django>=4.0,<4.1 + dj41: Django>=4.1,<4.2 djmaster: https://github.com/django/django/archive/refs/heads/main.zip postgres: psycopg2-binary mysql: mysqlclient