diff --git a/.github/workflows/compile_catalog.yml b/.github/workflows/compile_catalog.yml index 5e408784a..bcdea4cb8 100644 --- a/.github/workflows/compile_catalog.yml +++ b/.github/workflows/compile_catalog.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python 3.x uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - run: python -m pip install tox - name: Compile Catalog run: tox diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index c20062988..641b71915 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -16,12 +16,12 @@ jobs: - name: Set up Python 3.x uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - run: python -m pip install tox - - name: tox py39-flake8 + - name: tox py310-flake8 run: tox env: - TOXENV: py39-flake8 + TOXENV: py310-flake8 mypy: name: mypy @@ -32,7 +32,7 @@ jobs: - name: Set up Python 3.x uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - run: python -m pip install tox - name: tox mypy run: tox diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml index 85caad922..dd8cdd412 100644 --- a/.github/workflows/precommit.yml +++ b/.github/workflows/precommit.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python 3.x uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - run: python -m pip install tox - name: tox precommit run: tox diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index ff338210d..c2d51f210 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -18,6 +18,7 @@ jobs: - 3.7 - 3.8 - 3.9 + - "3.10" - pypy3 tox-django-version: - "22" @@ -58,7 +59,7 @@ jobs: max-parallel: 4 matrix: python-version: - - 3.9 + - "3.10" tox-django-version: - "32" steps: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index de6fa94e7..f4a3430fe 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python 3.x uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - run: python -m pip install tox - name: safety run: tox diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a182812..cfa5cb942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,27 @@ Changelog ========= +3.1.4 +----- + +Changes: + - Fix: set_default_site, improve django.contrib.sites application detection + - Improvement: documentation, Fix name of mixin in docs + - Improvement: mypy, type ignore backwards compatible imports + - Improvement: graph_models, add --rankdir to change graph direction + - Improvement: runserver_plus, Add --sql-truncate cli modifier + - Improvement: shell_plus, Add --sql-truncate cli modifier + + +3.1.3 +----- + +Changes: + - Fix: Django 3.2, Run tests against Django 3.2 + - Fix: Django 3.2, Handle warnings for default_app_config (#1654) + - Fix: sqldiff, Fix for missing field/index in model case + + 3.1.2 ----- diff --git a/README.rst b/README.rst index 784d095fa..37f8bc4d1 100644 --- a/README.rst +++ b/README.rst @@ -125,7 +125,7 @@ Or you can look at the docs/ directory in the repository. Support ======= -Django Extensions is free and always will be. It is development and maintained by developers in an Open Source manner. +Django Extensions is free and always will be. It is developed and maintained by developers in an Open Source manner. Any support is welcome. You could help by writing documentation, pull-requests, report issues and/or translations. Please remember that nobody is paid directly to develop or maintain Django Extensions so we do have to divide our time diff --git a/django_extensions/__init__.py b/django_extensions/__init__.py index 880264661..d4291672a 100644 --- a/django_extensions/__init__.py +++ b/django_extensions/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -VERSION = (3, 1, 3) +VERSION = (3, 1, 4) def get_version(version): diff --git a/django_extensions/management/commands/graph_models.py b/django_extensions/management/commands/graph_models.py index 21d477e2d..c70e16968 100644 --- a/django_extensions/management/commands/graph_models.py +++ b/django_extensions/management/commands/graph_models.py @@ -171,7 +171,14 @@ def __init__(self, *args, **kwargs): 'dest': 'arrow_shape', 'choices': ['box', 'crow', 'curve', 'icurve', 'diamond', 'dot', 'inv', 'none', 'normal', 'tee', 'vee'], 'help': 'Arrow shape to use for relations. Default is dot. Available shapes: box, crow, curve, icurve, diamond, dot, inv, none, normal, tee, vee.', - } + }, + '--rankdir': { + 'action': 'store', + 'default': 'TB', + 'dest': 'rankdir', + 'choices': ['TB', 'BT', 'LR', 'RL'], + 'help': 'Set direction of graph layout. Supported directions: "TB", "LR", "BT", "RL", corresponding to directed graphs drawn from top to bottom, from left to right, from bottom to top, and from right to left, respectively. Default is TB.' + }, } defaults = getattr(settings, 'GRAPH_MODELS', None) @@ -230,6 +237,9 @@ def handle(self, *args, **options): else: raise CommandError("Neither pygraphviz nor pydotplus could be found to generate the image. To generate text output, use the --json or --dot options.") + if options.get('rankdir') != 'TB' and output not in ["pydot", "pygraphviz", "dot"]: + raise CommandError("--rankdir is not supported for the chosen output format") + # Consistency check: Abort if --pygraphviz or --pydot options are set # but no outputfile is specified. Before 2.1.4 this silently fell back # to printind .dot format to stdout. diff --git a/django_extensions/management/commands/pipchecker.py b/django_extensions/management/commands/pipchecker.py index 0a8724e2e..5615d5099 100644 --- a/django_extensions/management/commands/pipchecker.py +++ b/django_extensions/management/commands/pipchecker.py @@ -22,15 +22,15 @@ try: from pip._internal.network.session import PipSession except ImportError: - from pip._internal.download import PipSession + from pip._internal.download import PipSession # type:ignore from pip._internal.req.req_file import parse_requirements from pip._internal.utils.misc import get_installed_distributions except ImportError: # pip < 10 try: - from pip import get_installed_distributions - from pip.download import PipSession - from pip.req import parse_requirements + from pip import get_installed_distributions # type:ignore + from pip.download import PipSession # type:ignore + from pip.req import parse_requirements # type:ignore except ImportError: raise CommandError("Pip version 6 or higher is required") diff --git a/django_extensions/management/commands/runserver_plus.py b/django_extensions/management/commands/runserver_plus.py index 3f69485b4..2c9af4384 100644 --- a/django_extensions/management/commands/runserver_plus.py +++ b/django_extensions/management/commands/runserver_plus.py @@ -150,6 +150,8 @@ def add_arguments(self, parser): help='Specifies an output file to send a copy of all messages (not flushed immediately).') parser.add_argument('--print-sql', action='store_true', default=False, help="Print SQL queries as they're executed") + parser.add_argument('--truncate-sql', action='store', type=int, + help="Truncate SQL queries to a number of characters.") parser.add_argument('--print-sql-location', action='store_true', default=False, help="Show location in code where SQL query generated from") cert_group = parser.add_mutually_exclusive_group() @@ -286,7 +288,9 @@ def postmortem(request, exc_type, exc_value, tb): self.addr = '::1' if self.use_ipv6 else '127.0.0.1' self._raw_ipv6 = True - with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"], print_sql_location=options["print_sql_location"], logger=logger.info, confprefix="RUNSERVER_PLUS"): + truncate = None if options["truncate_sql"] == 0 else options["truncate_sql"] + + with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"], print_sql_location=options["print_sql_location"], truncate=truncate, logger=logger.info, confprefix="RUNSERVER_PLUS"): self.inner_run(options) def get_handler(self, *args, **options): diff --git a/django_extensions/management/commands/set_default_site.py b/django_extensions/management/commands/set_default_site.py index 295d0aad6..1ea5fc273 100644 --- a/django_extensions/management/commands/set_default_site.py +++ b/django_extensions/management/commands/set_default_site.py @@ -3,6 +3,7 @@ from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from django.apps import apps from django_extensions.management.utils import signalcommand @@ -29,7 +30,7 @@ def add_arguments(self, parser): @signalcommand def handle(self, *args, **options): - if 'django.contrib.sites' not in settings.INSTALLED_APPS: + if not apps.is_installed('django.contrib.sites'): raise CommandError('The sites framework is not installed.') from django.contrib.sites.models import Site diff --git a/django_extensions/management/commands/shell_plus.py b/django_extensions/management/commands/shell_plus.py index c5a972826..dcb991174 100644 --- a/django_extensions/management/commands/shell_plus.py +++ b/django_extensions/management/commands/shell_plus.py @@ -84,6 +84,10 @@ def add_arguments(self, parser): default=False, help="Print SQL queries as they're executed" ) + parser.add_argument( + '--truncate-sql', action='store', type=int, + help="Truncate SQL queries to a number of characters." + ) parser.add_argument( '--print-sql-location', action='store_true', default=False, @@ -508,8 +512,9 @@ def handle(self, *args, **options): print_sql = getattr(settings, 'SHELL_PLUS_PRINT_SQL', False) runner = None runner_name = None + truncate = None if options["truncate_sql"] == 0 else options["truncate_sql"] - with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"] or print_sql, print_sql_location=options["print_sql_location"], confprefix="SHELL_PLUS"): + with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"] or print_sql, truncate=truncate, print_sql_location=options["print_sql_location"], confprefix="SHELL_PLUS"): SETTINGS_SHELL_PLUS = getattr(settings, 'SHELL_PLUS', None) def get_runner_by_flag(flag): diff --git a/django_extensions/management/debug_cursor.py b/django_extensions/management/debug_cursor.py index 0ca584b93..ad68cfd6a 100644 --- a/django_extensions/management/debug_cursor.py +++ b/django_extensions/management/debug_cursor.py @@ -8,13 +8,16 @@ from django.core.exceptions import ImproperlyConfigured from django.db.backends import utils +from django_extensions.settings import DEFAULT_PRINT_SQL_TRUNCATE_CHARS + @contextmanager def monkey_patch_cursordebugwrapper(print_sql=None, print_sql_location=False, truncate=None, logger=print, confprefix="DJANGO_EXTENSIONS"): if not print_sql: yield else: - truncate = getattr(settings, '%s_PRINT_SQL_TRUNCATE' % confprefix, 1000) + if truncate is None: + truncate = getattr(settings, '%s_PRINT_SQL_TRUNCATE' % confprefix, DEFAULT_PRINT_SQL_TRUNCATE_CHARS) # Code orginally from http://gist.github.com/118990 sqlparse = None diff --git a/django_extensions/management/modelviz.py b/django_extensions/management/modelviz.py index 2bbe0b9db..15552d07c 100644 --- a/django_extensions/management/modelviz.py +++ b/django_extensions/management/modelviz.py @@ -84,6 +84,7 @@ def __init__(self, app_labels, **kwargs): self.app_labels = [app.label for app in apps.get_app_configs()] else: self.app_labels = app_labels + self.rankdir = kwargs.get("rankdir") def generate_graph_data(self): self.process_apps() @@ -107,6 +108,7 @@ def get_graph_data(self, as_json=False): 'disable_fields': self.disable_fields, 'disable_abstract_fields': self.disable_abstract_fields, 'use_subgraph': self.use_subgraph, + 'rankdir': self.rankdir, } if as_json: diff --git a/django_extensions/settings.py b/django_extensions/settings.py index ec1b0ccab..4b71b677b 100644 --- a/django_extensions/settings.py +++ b/django_extensions/settings.py @@ -28,3 +28,5 @@ SQLITE_ENGINES = getattr(settings, 'DJANGO_EXTENSIONS_RESET_DB_SQLITE_ENGINES', DEFAULT_SQLITE_ENGINES) MYSQL_ENGINES = getattr(settings, 'DJANGO_EXTENSIONS_RESET_DB_MYSQL_ENGINES', DEFAULT_MYSQL_ENGINES) POSTGRESQL_ENGINES = getattr(settings, 'DJANGO_EXTENSIONS_RESET_DB_POSTGRESQL_ENGINES', DEFAULT_POSTGRESQL_ENGINES) + +DEFAULT_PRINT_SQL_TRUNCATE_CHARS = 1000 diff --git a/django_extensions/templates/django_extensions/graph_models/django2018/digraph.dot b/django_extensions/templates/django_extensions/graph_models/django2018/digraph.dot index 12a3bc8d6..38cb2af68 100644 --- a/django_extensions/templates/django_extensions/graph_models/django2018/digraph.dot +++ b/django_extensions/templates/django_extensions/graph_models/django2018/digraph.dot @@ -5,7 +5,8 @@ {% block digraph_options %}fontname = "Roboto" fontsize = 8 - splines = true{% endblock %} + splines = true + rankdir = "{{ rankdir }}"{% endblock %} node [{% block node_options %} fontname = "Roboto" diff --git a/django_extensions/templates/django_extensions/graph_models/original/digraph.dot b/django_extensions/templates/django_extensions/graph_models/original/digraph.dot index 1098f737d..ef4c36abf 100644 --- a/django_extensions/templates/django_extensions/graph_models/original/digraph.dot +++ b/django_extensions/templates/django_extensions/graph_models/original/digraph.dot @@ -5,7 +5,8 @@ {% block digraph_options %}fontname = "Helvetica" fontsize = 8 - splines = true{% endblock %} + splines = true + rankdir = "{{ rankdir }}"{% endblock %} node [{% block node_options %} fontname = "Helvetica" diff --git a/docs/conf.py b/docs/conf.py index fdc049bef..b6dba1ccf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,7 +47,7 @@ # The short X.Y version. version = '3.1' # The full version, including alpha/beta/rc tags. -release = '3.1.3' +release = '3.1.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/graph_models.rst b/docs/graph_models.rst index ad77a89b5..b1443f5b3 100644 --- a/docs/graph_models.rst +++ b/docs/graph_models.rst @@ -133,6 +133,12 @@ image by using the *graph_models* command:: # Create a graph with 'normal' arrow shape for relations $ ./manage.py graph_models -a --arrow-shape normal -o my_project_sans_foo_bar.png +:: + + # Create a graph with different layout direction, + # supported directions: "TB", "LR", "BT", "RL" + $ ./manage.py graph_models -a --rankdir BT -o my_project_sans_foo_bar.png + .. _GraphViz: http://www.graphviz.org/ diff --git a/docs/permissions.rst b/docs/permissions.rst index 9ae787b24..837e34126 100644 --- a/docs/permissions.rst +++ b/docs/permissions.rst @@ -10,7 +10,7 @@ query and limit access to certain views. Current Mixins --------------------------------- -* *UserPermissionMixin* - A Class Based View mixin that limits the accessibility to the view based on the "owner" of the view. +* *ModelUserFieldPermissionMixin* - A Class Based View mixin that limits the accessibility to the view based on the "owner" of the view. This will check if the currently logged in user (``self.request.user``) matches the owner of the model instance. By default, the "owner" will be called "user". @@ -33,11 +33,11 @@ By default, the "owner" will be called "user". from django.views.generic import UpdateView - from django_extensions.auth.mixins import UserPermissionMixin + from django_extensions.auth.mixins import ModelUserFieldPermissionMixin from .models import MyModel - class MyModelUpdateView(UserPermissionMixin, UpdateView): + class MyModelUpdateView(ModelUserFieldPermissionMixin, UpdateView): model = MyModel template_name = 'mymodels/update.html' model_permission_user_field = 'author' diff --git a/docs/shell_plus.rst b/docs/shell_plus.rst index e25451570..7f8326c72 100644 --- a/docs/shell_plus.rst +++ b/docs/shell_plus.rst @@ -436,3 +436,16 @@ You can also set the configuration option SHELL_PLUS_PRINT_SQL to omit the above # print SQL queries in shell_plus SHELL_PLUS_PRINT_SQL = True + +Printing SQL queries also comes with the possibility of specifying the maximum amount of characters to display: + + $ ./manage.py shell_plus --print-sql --truncate-sql + +`--truncate-sql` accepts an int value starting from 0 (which disables truncation). Defaults to 1000. + +You can also set the configuration option SHELL_PLUS_PRINT_SQL_TRUNCATE to omit the above command line option. + +:: + + # print SQL queries in shell_plus + SHELL_PLUS_PRINT_SQL_TRUNCATE = None diff --git a/requirements-dev.txt b/requirements-dev.txt index 0f56fbf66..b8f6dde06 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,3 +9,8 @@ requests mock pygments vobject + +types-pyOpenSSL +types-PyYAML +types-boto +types-requests diff --git a/setup.py b/setup.py index f15a33ad7..111be0f0e 100644 --- a/setup.py +++ b/setup.py @@ -127,6 +127,7 @@ def fullsplit(path, result=None): 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Utilities', diff --git a/tests/management/commands/shell_plus_tests/test_shell_plus.py b/tests/management/commands/shell_plus_tests/test_shell_plus.py index be59e900a..bd90ccd2f 100644 --- a/tests/management/commands/shell_plus_tests/test_shell_plus.py +++ b/tests/management/commands/shell_plus_tests/test_shell_plus.py @@ -41,6 +41,39 @@ def test_shell_plus_print_sql(capsys): assert re.search(r"SELECT\s+.+\s+FROM\s+.auth_user.\s+LIMIT\s+1", out) +@pytest.mark.django_db() +@override_settings(SHELL_PLUS_SQLPARSE_ENABLED=False, SHELL_PLUS_PYGMENTS_ENABLED=False) +def test_shell_plus_print_sql_truncate(capsys): + try: + from django.db import connection + from django.db.backends import utils + CursorDebugWrapper = utils.CursorDebugWrapper + force_debug_cursor = True if connection.force_debug_cursor else False + call_command("shell_plus", "--plain", "--print-sql", "--truncate-sql=0", "--command=User.objects.all().exists()") + finally: + utils.CursorDebugWrapper = CursorDebugWrapper + connection.force_debug_cursor = force_debug_cursor + + out, err = capsys.readouterr() + + assert re.search(r"SELECT\s+.+\s+FROM\s+.auth_user.\s+LIMIT\s+1", out) + + try: + from django.db import connection + from django.db.backends import utils + CursorDebugWrapper = utils.CursorDebugWrapper + force_debug_cursor = True if connection.force_debug_cursor else False + call_command("shell_plus", "--plain", "--print-sql", "--truncate-sql=4", "--command=User.objects.all().exists()") + finally: + utils.CursorDebugWrapper = CursorDebugWrapper + connection.force_debug_cursor = force_debug_cursor + + out, err = capsys.readouterr() + + assert re.search(r"SELE", out) + assert not re.search(r"SELEC", out) + + def test_shell_plus_plain_startup(): command = shell_plus.Command() command.tests_mode = True diff --git a/tests/management/commands/test_set_default_site.py b/tests/management/commands/test_set_default_site.py index 5d4950b07..eb596d6d2 100644 --- a/tests/management/commands/test_set_default_site.py +++ b/tests/management/commands/test_set_default_site.py @@ -82,3 +82,10 @@ def test_should_set_domain_only(self): self.assertEqual(result.name, 'example.com') self.assertEqual(result.domain, 'bar') + + def test_should_not_raise_if_sites_installed_through_appconfig(self): + with self.modify_settings(INSTALLED_APPS={ + 'append': 'django.contrib.sites.apps.SitesConfig', + 'remove': 'django.contrib.sites', + }): + call_command('set_default_site', '--name=foo', '--domain=foo.bar') diff --git a/tox.ini b/tox.ini index 0f76ad822..69647e83e 100644 --- a/tox.ini +++ b/tox.ini @@ -8,15 +8,15 @@ envlist = precommit safety mypy - {py37,py38,py39}-flake8 - {py36,py37,py38,py39,pypy}-dj22 - {py36,py37,py38,py39,pypy}-dj30 - {py36,py37,py38,py39,pypy}-dj31 - {py36,py37,py38,py39,pypy}-dj32 - {py38,py39,pypy}-djmaster - py39-dj32-postgres - py39-dj32-mysql - py39-djmaster-postgres + {py37,py38,py39,py310}-flake8 + {py36,py37,py38,py39,py310,pypy}-dj22 + {py36,py37,py38,py39,py310,pypy}-dj30 + {py36,py37,py38,py39,py310,pypy}-dj31 + {py36,py37,py38,py39,py310,pypy}-dj32 + {py38,py39,py310,pypy}-djmaster + py310-dj32-postgres + py310-dj32-mysql + py310-djmaster-postgres [testenv] commands = make test @@ -36,6 +36,7 @@ setenv = mysql: DJANGO_EXTENSIONS_DATABASE_NAME = {env:DJANGO_EXTENSIONS_DATABASE_NAME:django_extensions_test} deps = + pip >= 21.1 -rrequirements-dev.txt dj22: Django==2.2 dj30: Django>=3.0,<3.1 @@ -47,38 +48,52 @@ deps = [testenv:precommit] deps = + pip >= 21.1 pre-commit commands = pre-commit run -a [testenv:safety] deps = + pip >= 21.1 safety commands = safety check --full-report [testenv:mypy] deps = + pip >= 21.1 mypy + -rrequirements-dev.txt commands = mypy --ignore-missing-imports django_extensions [testenv:compile-catalog] whitelist_externals = make deps = + pip >= 21.1 Babel commands = make compile-catalog [testenv:py37-flake8] deps = + pip >= 21.1 flake8 commands = flake8 django_extensions tests [testenv:py38-flake8] deps = + pip >= 21.1 flake8 commands = flake8 django_extensions tests [testenv:py39-flake8] deps = + pip >= 21.1 + flake8 +commands = flake8 django_extensions tests + +[testenv:py310-flake8] +deps = + pip >= 21.1 flake8 commands = flake8 django_extensions tests