diff --git a/.editorconfig b/.editorconfig index 5c99f6582..736c9d3e9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# http://editorconfig.org +# https://editorconfig.org # Source: pydanny cookiecutter-django repo root = true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 34aba0598..941c30182 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -4,3 +4,4 @@ github: [trbs] patreon: djangoextensions open_collective: djangoextensions custom: ['https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=P57EJJ9QYL232'] +thanks_dev: u/gh/trbs diff --git a/.github/workflows/compile_catalog.yml b/.github/workflows/compile_catalog.yml index bcdea4cb8..039a721d1 100644 --- a/.github/workflows/compile_catalog.yml +++ b/.github/workflows/compile_catalog.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python 3.x - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" - run: python -m pip install tox - name: Compile Catalog run: tox diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 641b71915..03885c334 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -7,32 +7,32 @@ on: - main jobs: - flake8: - name: flake8 + ruff: + name: ruff runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python 3.x - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" - run: python -m pip install tox - - name: tox py310-flake8 + - name: tox ruff run: tox env: - TOXENV: py310-flake8 + TOXENV: ruff mypy: name: mypy runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python 3.x - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" - run: python -m pip install tox - name: tox mypy run: tox diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml index dd8cdd412..babc17ac4 100644 --- a/.github/workflows/precommit.yml +++ b/.github/workflows/precommit.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python 3.x - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.13" - run: python -m pip install tox - name: tox precommit run: tox diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index c2d51f210..117ca86a6 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -14,24 +14,31 @@ jobs: max-parallel: 4 matrix: python-version: - - 3.6 - - 3.7 - - 3.8 - 3.9 - - "3.10" - - pypy3 + - "3.12" + - "3.13" + # 2023-06-05: disabled pypy3.9 due to asgiref typing error + # - "pypy3.9" tox-django-version: - - "22" - - "30" - - "31" - - "32" + - "42" + - "51" + - "52" # GH Actions don't support something like allow-failure ? # - "master" + exclude: + - python-version: "3.9" + tox-django-version: "51" + - python-version: "3.9" + tox-django-version: "52" + - python-version: "3.13" + tox-django-version: "42" + - python-version: "pypy3.9" + tox-django-version: "42" steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: python -m pip install tox @@ -59,14 +66,14 @@ jobs: max-parallel: 4 matrix: python-version: - - "3.10" + - "3.13" tox-django-version: - - "32" + - "52" steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: python -m pip install tox diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml deleted file mode 100644 index f4a3430fe..000000000 --- a/.github/workflows/security.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Check Security Vulnerabilities - -on: - pull_request: - push: - branches: - - main - -jobs: - safety: - name: safety - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Set up Python 3.x - uses: actions/setup-python@v2 - with: - python-version: "3.10" - - run: python -m pip install tox - - name: safety - run: tox - env: - TOXENV: safety diff --git a/.gitignore b/.gitignore index 122246111..b6e8c9437 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,12 @@ venv* .idea/ htmlcov/ .coverage +.coverage.* .cache/ .mypy_cache/ .pytest_cache/ django-sample-app*/ *.swp *.swo +*.sqlite3 +.app-style.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f5983c241..38e3ced0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ -- repo: git://github.com/pre-commit/pre-commit-hooks - sha: v0.9.1 +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 hooks: - id: trailing-whitespace - id: check-added-large-files @@ -14,18 +15,31 @@ - id: check-yaml - id: detect-private-key - id: end-of-file-fixer - - id: fix-encoding-pragma - - id: flake8 + - id: mixed-line-ending + args: [ '--fix=lf' ] + description: Forces to replace line ending by the UNIX 'lf' character. - id: name-tests-test args: - --django exclude: ^tests/testapp|^tests/management/|^tests/collisions/|^tests/pythonrc.py|^tests/runner.py -- repo: git://github.com/Lucas-C/pre-commit-hooks.git - sha: v1.0.1 +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.11.4 hooks: - - id: forbid-crlf -- repo: git://github.com/trbs/pre-commit-hooks-trbs.git - sha: e233916fb2b4b9019b4a3cc0497994c7926fe36b + # Run the linter. + - id: ruff + # Run the formatter. + - id: ruff-format + args: + - --diff +- repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.5.0 + hooks: + - id: pyproject-fmt + args: + - --check +- repo: https://github.com/trbs/pre-commit-hooks-trbs.git + rev: 1.2.4 hooks: - id: forbid-executables exclude: manage.py|setup.py diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..9086a09bf --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,22 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.12" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index cfa5cb942..d3ad6aec6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,147 +1,233 @@ Changelog ========= +See https://github.com/django-extensions/django-extensions/releases + +4.1 +--- + +Changes: + +- Add: show_permissions command (#1920) +- Improvement: graph_models, style per app (#1848) +- Fix: JSONField, bulk_update's (#1924) + +4.0 +--- + +Changes: + +- Improvement: Support for Python 3.12 and 3.13 +- Improvement: Support for Django 5.x +- Improvement: Switch from setup.{cfg,py} to pyproject.toml +- Improvement: graph_models, Add option to display field choices in graph_models (#1854) +- Improvement: graph_models, Add webp support (#1857) +- Improvement: graph_models, Support for ordering edges on pydot/dot/graphviz (#1914) +- Improvement: mail_debug, Update mail_debug command to use aiosmtpd (#1880) +- Improvement: shell_plus, Improve error message for missing import (#1898) +- Improvement: reset_db, Add reset_db support for django_tenants (#1855) +- Improvement: docs, various improvements (#1852, #1888, #1882, #1901, #1912, #1913) +- Improvement: jobs, Handle non-package modules when looking for job definitions (#1887) +- Improvement: Add django-prometheus DB backends support (#1800) +- Improvement: Call post_command when the command raises an unhandled exception (#1837) +- Fix: sqldiff, do not consider ('serial', 'integer') nor ('bigserial', 'bigint') as a `field-type-differ` (#1867) +- Fix: shell_plus, Fix start up order and add history (#1869) +- Remove pipchecker and associated tests (#1906) +- Following Django's release numbering style more closely (see https://docs.djangoproject.com/en/5.2/internals/release-process/ ) + +3.2.3 +----- + +Changes: + +- Improvement: Add support for psycopg3 (#1814) +- Improvement: runserver_plus, autoreload on template change (#1796) +- Improvement: highlighting, test_should_highlight_bash_syntax_without_name to include whitespace spans (#1797) +- Improvement: tests, add Python 3.11 to tox and actions to formally support python 3.11 (#1786) +- Improvement: runserver_plus, Send the file_changed event when a reload is triggered (#1775) +- Improvement: runserver_plus, Add REMOTE_USER to werkzeug environment (#1708) +- Improvement: pipchecker, force pip to use pkg_resources as backend for resolving distributions (#1782) +- Fix: Fix error with lack of PosixPath support (#1785) +- Fix: Cleanup http: links (#1798) + +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 +----- + +Changes: + +- Improvement: Django 4 support +- Improvement: Accept both --no-input and --noinput +- Improvement: sqldsn, Added more styles to the sqldsn management command +- Improvement: graph_models, Flag for to color code relations based on on_delete +- Improvement: graph_models, Add --relation-fields-only flag +- Improvement: RandomCharField, allow keeping default values +- Fix: HexValidator, Max length validation +- 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 +----- + +Changes: + +- Fix: pipchecker, crude way slow down to avoid HTTPTooManyRequests +- Fix: pipchecker, fix for removed get_installed_distributions function + 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 +- 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 +- 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 ----- Changes: - - Improvement: shell_plus, not save ipython history when using Jupyter - - Improvement: docs, fix spelling mistakes - - Improvement: tests, move to Github Actions instead of Travis - - Improvement: drop_test_database, delete all cloned test databases (#1637) - - Improvement: setup.py, Added minimum Django>=2.2 version to PyPI package - - Improvement: shell_plus, fix --command globals / locals error +- Improvement: shell_plus, not save ipython history when using Jupyter +- Improvement: docs, fix spelling mistakes +- Improvement: tests, move to Github Actions instead of Travis +- Improvement: drop_test_database, delete all cloned test databases (#1637) +- Improvement: setup.py, Added minimum Django>=2.2 version to PyPI package +- Improvement: shell_plus, fix --command globals / locals error 3.1.1.post1 ----------- Changes: - - Improvement: setup.py, Added minimum Django>=2.2 version to PyPI package +- Improvement: setup.py, Added minimum Django>=2.2 version to PyPI package 3.1.1 ----- Changes: - - Improvement: graph_models, add option --app-labels - - Improvement: shell_plus, update shell_plus for jupyterlab 3 - - Improvement: tests, add Python 3.9 +- Improvement: graph_models, add option --app-labels +- Improvement: shell_plus, update shell_plus for jupyterlab 3 +- Improvement: tests, add Python 3.9 3.1.0 ----- Changes: - - Improvement: pipchecker, sleep 60s if pypi raises a fault - - Improvement: add django_zero_downtime_migrations to list of supported postgresql engines - - Improvement: use list of supported database engines from settings for all database commands - - Improvement: reset_db, documentation - - Fix: tests, Python 3.9 fixes for some tests - - Fix: runserver_plus, parsing of RUNSERVER_PLUS_EXTRA_FILES +- Improvement: pipchecker, sleep 60s if pypi raises a fault +- Improvement: add django_zero_downtime_migrations to list of supported postgresql engines +- Improvement: use list of supported database engines from settings for all database commands +- Improvement: reset_db, documentation +- Fix: tests, Python 3.9 fixes for some tests +- Fix: runserver_plus, parsing of RUNSERVER_PLUS_EXTRA_FILES 3.0.9 ----- Changes: - - Improvement: runserver_plus, survive syntax and configuration errors part II - - Improvement: tests, refactor test runner - - Improvement: sqlcreate, support postgresql unix domain socket +- Improvement: runserver_plus, survive syntax and configuration errors part II +- Improvement: tests, refactor test runner +- Improvement: sqlcreate, support postgresql unix domain socket 3.0.8 ----- Changes: - - Improvement: setup.cfg, remove universal flag from wheel, we only support Python 3 and up - - Improvement: sqlcreate, fixed mentioned of old syncdb - - Fix: runserver_plus, stop catching SyntaxError since reload for it was not working properly +- Improvement: setup.cfg, remove universal flag from wheel, we only support Python 3 and up +- Improvement: sqlcreate, fixed mentioned of old syncdb +- Fix: runserver_plus, stop catching SyntaxError since reload for it was not working properly 3.0.7 ----- Changes: - - Improvement: runserver_plus, gh #1575 survive syntax and configuration errors - - Improvement: runscript, use exit-code 1 if script is not found +- Improvement: runserver_plus, gh #1575 survive syntax and configuration errors +- Improvement: runscript, use exit-code 1 if script is not found 3.0.6 ----- Changes: - - Improvement: runscript, add --continue-on-error unless set runscript will exit on errors - - Improvement: runscript, allow to return exit-code - - Improvement: runscript, support raise CommandError(... returncode=...) - - Improvement: runscript, run Django checks() and check_migrations() before executing scripts - - Improvement: shell_plus, set application name on all postgresql backends +- Improvement: runscript, add --continue-on-error unless set runscript will exit on errors +- Improvement: runscript, allow to return exit-code +- Improvement: runscript, support raise CommandError(... returncode=...) +- Improvement: runscript, run Django checks() and check_migrations() before executing scripts +- Improvement: shell_plus, set application name on all postgresql backends 3.0.5 ----- Changes: - - Fix: runserver_plus, exceptions must derive from BaseException error +- Fix: runserver_plus, exceptions must derive from BaseException error 3.0.4 ----- Changes: - - Various cleanups - - Deprecated using `--router` instead use `--database` - - Locales: Indonesian and Polish, updated - - Improvement: show_dsn, fix crash with django-postgres-extra - - Improvement: print_settings, added wildcard support - - Improvement: print_settings, added --fail option - - Improvement: delete_squashed_migrations, add --database option - - Improvement: runserver_plus, added RUNSERVER_PLUS_EXTRA_FILES setting - - Improvement: runserver_plus, added runserver_plus_started signal +- Various cleanups +- Deprecated using `--router` instead use `--database` +- Locales: Indonesian and Polish, updated +- Improvement: show_dsn, fix crash with django-postgres-extra +- Improvement: print_settings, added wildcard support +- Improvement: print_settings, added --fail option +- Improvement: delete_squashed_migrations, add --database option +- Improvement: runserver_plus, added RUNSERVER_PLUS_EXTRA_FILES setting +- Improvement: runserver_plus, added runserver_plus_started signal 3.0.3 ----- Changes: - - New: InternalIPS, allows to specify CIDRs for INTERNAL_IPS - - Docs: restructure toctree +- New: InternalIPS, allows to specify CIDRs for INTERNAL_IPS +- Docs: restructure toctree 3.0.2 ----- Changes: - - Fix: shell_plus, fix honouring SHELL_PLUS in settings.py +- Fix: shell_plus, fix honouring SHELL_PLUS in settings.py 3.0.1 ----- Changes: - - Fix: setup.py, add python_requires and remove legacy trove classifiers +- Fix: setup.py, add python_requires and remove legacy trove classifiers 3.0.0 ----- @@ -149,222 +235,222 @@ Changes: This is the first Django Extensions release which only targets Django 2.2 and above. It drops official support for Python 2.7. - Changes: - - Removal of Python 2 support - - Removal of deprecated keyczar encrypted fields EncryptedTextField and EncryptedCharField - - Removal of deprecated passwd command - - Removal of truncate_letters filter - - Change: TimeStampedModel; Removed default ordering on abstract model - - New: DjangoExtensionsConfig AppConfig - - New: shell_plus, JupyterLab support - - New: list_signals, List all signals by model and signal type - - Improvement: shell_plus, use -- to directly pass additional arguments to Jupyter - - Improvement: shell_plus, improvements to MySQL support - - Improvement: jobs, use logging to record errors - - Improvement: syncdata, added --remove-before flag - - Improvement: graph_models, add field and model to template context - - Fix: syncdata, fix non existent field in fixture data - - Fix: pipchecker, compatibility with pip 20.1 +- Removal of Python 2 support +- Removal of deprecated keyczar encrypted fields EncryptedTextField and EncryptedCharField +- Removal of deprecated passwd command +- Removal of truncate_letters filter +- Change: TimeStampedModel; Removed default ordering on abstract model +- New: DjangoExtensionsConfig AppConfig +- New: shell_plus, JupyterLab support +- New: list_signals, List all signals by model and signal type +- Improvement: shell_plus, use -- to directly pass additional arguments to Jupyter +- Improvement: shell_plus, improvements to MySQL support +- Improvement: jobs, use logging to record errors +- Improvement: syncdata, added --remove-before flag +- Improvement: graph_models, add field and model to template context +- Fix: syncdata, fix non existent field in fixture data +- Fix: pipchecker, compatibility with pip 20.1 2.2.9 ----- Changes: - - Fix: shell_plus, move notebook down the list of preferred shells - - Fix: sqldiff, fix KeyError when detecting missing (unique) indexes - - Improvement: encrypted fields, make it harder to use deprecated keyczar fields - - Locale: Removed empty localizations +- Fix: shell_plus, move notebook down the list of preferred shells +- Fix: sqldiff, fix KeyError when detecting missing (unique) indexes +- Improvement: encrypted fields, make it harder to use deprecated keyczar fields +- Locale: Removed empty localizations 2.2.8 ----- Changes: - - Locale: zh_Hans, removed as it generated UnicodeDecodeError errors (#1478) +- Locale: zh_Hans, removed as it generated UnicodeDecodeError errors (#1478) 2.2.7 ----- Changes: - - Improvement: shell_plus, #865 always add manage.py basedir to path for notebook kernel - - Improvement: docs, add zh-Hans locale - - Improvement: runserver_plus, fix broken import for werkzeug v1.0.0 - - Improvement: runserver_plus, #1461 fix always trying to load StaticFilesHandler - - Improvement: pipchecker, #1471 fix import of PipSession +- Improvement: shell_plus, #865 always add manage.py basedir to path for notebook kernel +- Improvement: docs, add zh-Hans locale +- Improvement: runserver_plus, fix broken import for werkzeug v1.0.0 +- Improvement: runserver_plus, #1461 fix always trying to load StaticFilesHandler +- Improvement: pipchecker, #1471 fix import of PipSession 2.2.6 ----- Changes: - - Improvement: travis, update pypy and pypy3 versions - - Improvement: shell_plus, ability to print location/traceback besides sql - - Improvement: runserver_plus, ability to print location/traceback besides sql - - Improvement: UniqueFieldMixin, Support Django 2.2 UniqueConstraint.condition - - Improvement: DEFAULT_MYSQL_ENGINES, add mysql.connector.django - - Improvement: shell_plus, allow setting SHELL_PLUS="notebook" - - Improvement: shell_plus, add -c/--command to shell_plus mirroring django's shell command - - Fix: shell_plus, fix postgresql debug wrapper on django 3.0 or higher - - Fix: runserver_plus, fix postgresql debug wrapper on django 3.0 or higher +- Improvement: travis, update pypy and pypy3 versions +- Improvement: shell_plus, ability to print location/traceback besides sql +- Improvement: runserver_plus, ability to print location/traceback besides sql +- Improvement: UniqueFieldMixin, Support Django 2.2 UniqueConstraint.condition +- Improvement: DEFAULT_MYSQL_ENGINES, add mysql.connector.django +- Improvement: shell_plus, allow setting SHELL_PLUS="notebook" +- Improvement: shell_plus, add -c/--command to shell_plus mirroring django's shell command +- Fix: shell_plus, fix postgresql debug wrapper on django 3.0 or higher +- Fix: runserver_plus, fix postgresql debug wrapper on django 3.0 or higher 2.2.5 ----- Changes: - - Improvement: travis, add Python 3.8 - - Improvement: setup.py, update classifiers +- Improvement: travis, add Python 3.8 +- Improvement: setup.py, update classifiers 2.2.4 ----- Changes: - - Improvement: RandomCharField, Support unique_together - - Improvement: export_emails, add settings for overriding queryset fields, order_by and the full_name function +- Improvement: RandomCharField, Support unique_together +- Improvement: export_emails, add settings for overriding queryset fields, order_by and the full_name function 2.2.3 ----- Changes: - - Fix: admin widgets, fix import of static template tag (part 2) + +- Fix: admin widgets, fix import of static template tag (part 2) 2.2.2 ----- Changes: - - Fix: autoslugfield, find unique method overrideable - - Fix: notes, do not replace dot in template dirs - - Fix: admin widgets, fix import of static template tag - - Improvement: print_user_for_session, use session backend - - Improvement: sqlcreate, postgis support - - Improvement: graph_models, permit combination of includes and excludes - - Improvement: Adds missing GIS engine to DEFAULT_MYSQL_ENGINES - - Improvement: sqldiff, use lowercase field names in MySQL - - Improvement: sqldiff, mysql code could duplicate AUTO_INCREMENT and UNSIGNED statements +- Fix: autoslugfield, find unique method overrideable +- Fix: notes, do not replace dot in template dirs +- Fix: admin widgets, fix import of static template tag +- Improvement: print_user_for_session, use session backend +- Improvement: sqlcreate, postgis support +- Improvement: graph_models, permit combination of includes and excludes +- Improvement: Adds missing GIS engine to DEFAULT_MYSQL_ENGINES +- Improvement: sqldiff, use lowercase field names in MySQL +- Improvement: sqldiff, mysql code could duplicate AUTO_INCREMENT and UNSIGNED statements 2.2.1 ----- Changes: - - Fix: tests, support for newer versions of pytest - - Fix: tests, disable test with drf dependency for older python versions +- Fix: tests, support for newer versions of pytest +- Fix: tests, disable test with drf dependency for older python versions 2.2.0 ----- Changes: - - Fix: removing wrongly released text_tags template - - Fix: graph_models, support for Python <3.6 - - Improvement: ForeignKeySearchInput, wrap media files in static() - - Improvement: UniqField, added tests - - Improvement: dumpscript, fix orm_item_locator to use dateutil - - Improvement: graph_models, added argument to change arrow_shape +- Fix: removing wrongly released text_tags template +- Fix: graph_models, support for Python <3.6 +- Improvement: ForeignKeySearchInput, wrap media files in static() +- Improvement: UniqField, added tests +- Improvement: dumpscript, fix orm_item_locator to use dateutil +- Improvement: graph_models, added argument to change arrow_shape 2.1.9 ----- Changes: - - Fix: show_urls, fix for traceback on multi language sites - - Improvement: reset_db, fix typo's in help test +- Fix: show_urls, fix for traceback on multi language sites +- Improvement: reset_db, fix typo's in help test 2.1.8 ----- Changes: - - New: HexValidator, validate hex strings - - Improvement: reset_db, move settings to `django_settings.settings` which makes it easier to override. - - Improvement: AutoSlugField, extend support for custom slugify function - - Fix: runprofileserver, fix autoreloader for newer Django versions +- New: HexValidator, validate hex strings +- Improvement: reset_db, move settings to `django_settings.settings` which makes it easier to override. +- Improvement: AutoSlugField, extend support for custom slugify function +- Fix: runprofileserver, fix autoreloader for newer Django versions 2.1.7 ----- Changes: - - New: test, many many more tests :-) thanks everybody - - New: docs, many documentation updates - - New: graph_model, add simple theming support and django2018 theme - - Improvement: ModificationDateTimeField, make modificationfield name modifiable - - Improvement: graph_model, option to not showrelations labels in the graph - - Improvement: reset_db, allow to override list of backends for database engines - - Improvement: reset_db, add psqlextra backend - - Improvement: runserver_plus, idle support - - Improvement: generate_secret_key, removed get_random_string in favour of get_random_secret_key - - Improvement: update_permissions, add create-only and update-only flags - - Improvement: update_permissions, update changed names of permission to match correct permission name - - Improvement: syncdata, add --database option - - Improvement: runscript, allow to override RUNSCRIPT_SCRIPT_DIR - - Fix: create_command, fix mknod error on macos - - Fix: runserver_plus, fix in resolving ssl certificate path - - Fix: sqldiff, fix hstorefield - - Deprecate: truncate_letters, use Django's truncatechars - - Deprecate: passwd, use Django's changepassword - - Deprecate: Keyczar encrypted fields, Keyczar is abandoned / deprecated +- New: test, many many more tests :-) thanks everybody +- New: docs, many documentation updates +- New: graph_model, add simple theming support and django2018 theme +- Improvement: ModificationDateTimeField, make modificationfield name modifiable +- Improvement: graph_model, option to not showrelations labels in the graph +- Improvement: reset_db, allow to override list of backends for database engines +- Improvement: reset_db, add psqlextra backend +- Improvement: runserver_plus, idle support +- Improvement: generate_secret_key, removed get_random_string in favour of get_random_secret_key +- Improvement: update_permissions, add create-only and update-only flags +- Improvement: update_permissions, update changed names of permission to match correct permission name +- Improvement: syncdata, add --database option +- Improvement: runscript, allow to override RUNSCRIPT_SCRIPT_DIR +- Fix: create_command, fix mknod error on macos +- Fix: runserver_plus, fix in resolving ssl certificate path +- Fix: sqldiff, fix hstorefield +- Deprecate: truncate_letters, use Django's truncatechars +- Deprecate: passwd, use Django's changepassword +- Deprecate: Keyczar encrypted fields, Keyczar is abandoned / deprecated 2.1.6 ----- Changes: - - Fix: runserver_plus, auto_reloader fix for compatibility with Django 2.2 - - New: test, many many more tests :-) thanks @kuter +- Fix: runserver_plus, auto_reloader fix for compatibility with Django 2.2 +- New: test, many many more tests :-) thanks @kuter 2.1.5 ----- Changes: - - New: ipdb, pdb and wdb filters - - Fix: ForeignKeySearchInput, error with widget render(...) parameters on Django 2.1 - - Fix: pipchecker, unsupported format string passed to NoneType.format error - - Tests: bunch of new test cases +- New: ipdb, pdb and wdb filters +- Fix: ForeignKeySearchInput, error with widget render(...) parameters on Django 2.1 +- Fix: pipchecker, unsupported format string passed to NoneType.format error +- Tests: bunch of new test cases 2.1.4 ----- Changes: - - Fix: null_technical_500_response, handle function-based middleware - - Fix: shell_plus, fix #1261 check for --notebook-dir=... argument style - - Fix: graph_models, Excluded models displayed as an underscore - - Fix: set_fake_password, requires_model_validation has been replaced with requires_system_checks since 1.9 - - Docs: admin_generator, new documentation and examples - - Improvement: JSONField, use new from_db_value syntax on Django 2 and up - - Improvement: EncryptedTextField, use new from_db_value syntax on Django 2 and up - - Improvement: graph_models, add --dot option - - Improvement: graph_models, allow to redirect (text) output to file - - Improvement: sqldiff, better support for indexes, index_together and unique_together +- Fix: null_technical_500_response, handle function-based middleware +- Fix: shell_plus, fix #1261 check for --notebook-dir=... argument style +- Fix: graph_models, Excluded models displayed as an underscore +- Fix: set_fake_password, requires_model_validation has been replaced with requires_system_checks since 1.9 +- Docs: admin_generator, new documentation and examples +- Improvement: JSONField, use new from_db_value syntax on Django 2 and up +- Improvement: EncryptedTextField, use new from_db_value syntax on Django 2 and up +- Improvement: graph_models, add --dot option +- Improvement: graph_models, allow to redirect (text) output to file +- Improvement: sqldiff, better support for indexes, index_together and unique_together 2.1.3 ----- Changes: - - Fix: Readme, add direct link to screencast video - - Fix: graph_models, regression under Python 2 - - Fix: ForeignKeyAutocompleteAdmin, 2.0.8 breaks ForeignKeyAutocompleteAdmin - - Fix: AutoSlugField, fix regression when copying an autoslug model require the explicit clearing of the slug if it needs to be recalculated - - Fix: technical_response, check for AttributeError - - Improvement: graph_models, Add feature disable_abstract_fields - - Improvement: AutoSlugField, Add overwrite_on_add - - Improvement: runscript, Improve module existence test in runscript +- Fix: Readme, add direct link to screencast video +- Fix: graph_models, regression under Python 2 +- Fix: ForeignKeyAutocompleteAdmin, 2.0.8 breaks ForeignKeyAutocompleteAdmin +- Fix: AutoSlugField, fix regression when copying an autoslug model require the explicit clearing of the slug if it needs to be recalculated +- Fix: technical_response, check for AttributeError +- Improvement: graph_models, Add feature disable_abstract_fields +- Improvement: AutoSlugField, Add overwrite_on_add +- Improvement: runscript, Improve module existence test in runscript 2.1.2 ----- Changes: - - Fix: AutoSlugField, fix check on list or tuple type +- Fix: AutoSlugField, fix check on list or tuple type 2.1.1 ----- @@ -372,29 +458,29 @@ Changes: Removed support for Django versions before 1.11 Changes: - - Fix: foreignkey_searchinput, remove unnecessary img tag - - Fix: sqldiff, fix deprecated get_indexes call - - Fix: AutoSlugField, check that any non-callable value passed to populate_from is a string type - - Fix: tests, fix ChangingDirectoryTests: cd back in tearDown - - Fix: show_template_tags, should handle AppConfig class in INSTALLED applications - - Improvement: runserver_plus, reduce reraise pollution in traceback page - - Improvement: dumpscript, prevent many2many field with custom intermediate models to be added directly on the parent model - - Docs: fix typos +- Fix: foreignkey_searchinput, remove unnecessary img tag +- Fix: sqldiff, fix deprecated get_indexes call +- Fix: AutoSlugField, check that any non-callable value passed to populate_from is a string type +- Fix: tests, fix ChangingDirectoryTests: cd back in tearDown +- Fix: show_template_tags, should handle AppConfig class in INSTALLED applications +- Improvement: runserver_plus, reduce reraise pollution in traceback page +- Improvement: dumpscript, prevent many2many field with custom intermediate models to be added directly on the parent model +- Docs: fix typos 2.1.0 ----- Changes: - - Fix: travis +- Fix: travis 2.0.9 ----- Changes: - - Improvement: use README as project description on PyPI +- Improvement: use README as project description on PyPI 2.0.8 ----- @@ -402,118 +488,118 @@ Changes: Please stop using ForeignKeyAutocompleteAdmin edition :-) Changes: - - Fix: special markers in runserver_plus.rst - - Fix: shell_plus, refactor reading pythonrc file outside of exec(compile(...)) - - Fix: reset_db, fix default utf8 support - - Fix: autoslugfield, Fix autoslug generation when existing model is copied - - Improvement: Cleanup management commands options after argparse migration #916 - - Improvement: sqldiff, add more tests - - Improvement: sqldiff, add DurationField and SearchVectorField - - Improvement: shell_plus, add more tests - - Improvement: shell_plus, backport macos fix for tab completion - - Improvement: clear_cache, add --all option - - Improvement: pipchecker, treat dev versions as unstable - - Deprecation: ForeignKeyAutocompleteAdmin, Django 2.0 has similar capabilities, which are much better supported. +- Fix: special markers in runserver_plus.rst +- Fix: shell_plus, refactor reading pythonrc file outside of exec(compile(...)) +- Fix: reset_db, fix default utf8 support +- Fix: autoslugfield, Fix autoslug generation when existing model is copied +- Improvement: Cleanup management commands options after argparse migration #916 +- Improvement: sqldiff, add more tests +- Improvement: sqldiff, add DurationField and SearchVectorField +- Improvement: shell_plus, add more tests +- Improvement: shell_plus, backport macos fix for tab completion +- Improvement: clear_cache, add --all option +- Improvement: pipchecker, treat dev versions as unstable +- Deprecation: ForeignKeyAutocompleteAdmin, Django 2.0 has similar capabilities, which are much better supported. 2.0.7 ----- Changes: - - Fix: pipchecker, pip 10.0.0 compatibility - - Fix: sqldiff, improve support of GIS fields by using Django introspection - - Fix: shell_plus, fix bug in windows when PYTHONPATH is defined - - Fix: shell_plus, Call execute on CursorWrapper instead of directly on cursor to ensure wrappers are run - - Fix: runserver_plus, Call execute on CursorWrapper instead of directly on cursor to ensure wrappers are run - - Improvement: sqldiff, drop old compatibility code - - Improvement: ForeignKeyAutocompleteAdminMixin, improvements for Django >1.9 +- Fix: pipchecker, pip 10.0.0 compatibility +- Fix: sqldiff, improve support of GIS fields by using Django introspection +- Fix: shell_plus, fix bug in windows when PYTHONPATH is defined +- Fix: shell_plus, Call execute on CursorWrapper instead of directly on cursor to ensure wrappers are run +- Fix: runserver_plus, Call execute on CursorWrapper instead of directly on cursor to ensure wrappers are run +- Improvement: sqldiff, drop old compatibility code +- Improvement: ForeignKeyAutocompleteAdminMixin, improvements for Django >1.9 2.0.6 ----- Changes: - - Fix: shell_plus, Fix of deprecation warning in collision resolvers +- Fix: shell_plus, Fix of deprecation warning in collision resolvers 2.0.5 ----- Changes: - - Improvement: setup.py, Use PEP 508 when setuptools is version 36 or higher should fix issues with pipenv - - Fix: docs, Docs should show that django 2.0 is supported +- Improvement: setup.py, Use PEP 508 when setuptools is version 36 or higher should fix issues with pipenv +- Fix: docs, Docs should show that django 2.0 is supported 2.0.4 ----- Changes: - - Fix: setup.py, fix installation of typing in python < 3.5 +- Fix: setup.py, fix installation of typing in python < 3.5 2.0.3 ----- Changes: - - Fix: shell_plus, python 2.7 support broken due to use of Python3 super() +- Fix: shell_plus, python 2.7 support broken due to use of Python3 super() 2.0.2 ----- Changes: - - Improvement: sqldiff, add --include-defaults to include default value in missing field for sqldiff #1064 +- Improvement: sqldiff, add --include-defaults to include default value in missing field for sqldiff #1064 2.0.1 ----- Changes: - - Fix: setup.py, do not include `typing` requirement in recent versions of Python - - Improvement: shell_plus, add support for using -- to pass cli argument directly to underlying python shell implementation - - New: generate_password, Generates a new password based on `BaseUserManager.make_random_password` +- Fix: setup.py, do not include `typing` requirement in recent versions of Python +- Improvement: shell_plus, add support for using -- to pass cli argument directly to underlying python shell implementation +- New: generate_password, Generates a new password based on `BaseUserManager.make_random_password` 2.0.0 ----- Changes: - - Fix: runserver_plus, for 1.11 still using MIDDLEWARE_CLASSES - - Fix: show_urls, Fix display in Django 2.0 - - Fix: validate_templates, remove realpath in validate_templates - - Fix: sqldiff, bug with including proxy models in sqldiff output - - Improvement: shell_plus, allow configurating of sqlparse formatting and pygments formatting - - Improvement: shell_plus, add collision resolvers based on app label - - Improvement: shell_plus, automatic importing of subclasses defined in SHELL_PLUS_SUBCLASSES_IMPORT - - New: reset_schema, simple command to recreate public schema in PostgreSQL - - Docs: fix links to Werkzeug documentation +- Fix: runserver_plus, for 1.11 still using MIDDLEWARE_CLASSES +- Fix: show_urls, Fix display in Django 2.0 +- Fix: validate_templates, remove realpath in validate_templates +- Fix: sqldiff, bug with including proxy models in sqldiff output +- Improvement: shell_plus, allow configurating of sqlparse formatting and pygments formatting +- Improvement: shell_plus, add collision resolvers based on app label +- Improvement: shell_plus, automatic importing of subclasses defined in SHELL_PLUS_SUBCLASSES_IMPORT +- New: reset_schema, simple command to recreate public schema in PostgreSQL +- Docs: fix links to Werkzeug documentation 1.9.9 ----- Changes: - - Fix: runserver_plus, fix for Django 2.0 middleware handling - - Fix: shell_plus, fixed app_name resolving - - Fix: AutoSlugField, deconstruct did not match construction values - - Fix: runjob, not compatible with apps that use AppConfig in INSTALLED_APPS - - Improvement: runserver_plus, added configuring paths to certificates - - Improvement: sample.py template, add newline to avoid linter warnings - - Improvement: jobs, add integration tests for runjob and runjobs management commands - - New: merge_model_instances, new management command for de-duplicating model instances +- Fix: runserver_plus, fix for Django 2.0 middleware handling +- Fix: shell_plus, fixed app_name resolving +- Fix: AutoSlugField, deconstruct did not match construction values +- Fix: runjob, not compatible with apps that use AppConfig in INSTALLED_APPS +- Improvement: runserver_plus, added configuring paths to certificates +- Improvement: sample.py template, add newline to avoid linter warnings +- Improvement: jobs, add integration tests for runjob and runjobs management commands +- New: merge_model_instances, new management command for de-duplicating model instances 1.9.8 ----- Changes: - - Fix: show_urls, fix for Django 2.0 (Locale URL Resolvers are still broken) - - Fix: runserver_plus, fix rendering of ipv6 link - - Improvement: validate_templates, allow relative paths - - Improvement: validate_templates, automatically include app templates - - Improvement: pip_checker, could not find some packages - - Docs: shell_plus, `--print-sql` usage clearification +- Fix: show_urls, fix for Django 2.0 (Locale URL Resolvers are still broken) +- Fix: runserver_plus, fix rendering of ipv6 link +- Improvement: validate_templates, allow relative paths +- Improvement: validate_templates, automatically include app templates +- Improvement: pip_checker, could not find some packages +- Docs: shell_plus, `--print-sql` usage clearification 1.9.7 ----- @@ -522,52 +608,51 @@ This release add checking types with MyPy to the test suite. At this point only a few lines of code are explicitly typed. Changes: - - Improvement: shell_plus, Collision resolver implemented. - - Improvement: shell_plus, Skipping all models importing feature added. - - Improvement: runscript, Script execution directory policy feature added. - - django-extensions now requires the [typing](https://pypi.python.org/pypi/typing) package. +- Improvement: shell_plus, Collision resolver implemented. +- Improvement: shell_plus, Skipping all models importing feature added. +- Improvement: runscript, Script execution directory policy feature added. +- django-extensions now requires the [typing](https://pypi.python.org/pypi/typing) package. 1.9.6 ----- Fix boo-boo with release version in django_extensions/__init__.py - 1.9.4 ----- Changes: - - Fix missing test case +- Fix missing test case 1.9.3 ----- Changes: - - Tests: shell_plus, simple test for get_imported_objects +- Tests: shell_plus, simple test for get_imported_objects 1.9.2 ----- Changes: - - Fix: mail_debug, regression in mail_debug for older Pythons - - Fix: shell_plus, SyntaxError on exec(), python compatibility - - Fix: ForeignKeyAutocompleteAdminMixin, use text/plain +- Fix: mail_debug, regression in mail_debug for older Pythons +- Fix: shell_plus, SyntaxError on exec(), python compatibility +- Fix: ForeignKeyAutocompleteAdminMixin, use text/plain 1.9.1 ----- Changes: - - Fix: graph_models, fix json option - - Fix: runserver_plus, avoid duplicate messages logged to console - - Fix: mail_debug, python3 fix - - Improvement: sqldiff, basic support for array types in postgresql - - Improvement: runscript, handle import errors better - - Docs: updated documentation for model extensions +- Fix: graph_models, fix json option +- Fix: runserver_plus, avoid duplicate messages logged to console +- Fix: mail_debug, python3 fix +- Improvement: sqldiff, basic support for array types in postgresql +- Improvement: runscript, handle import errors better +- Docs: updated documentation for model extensions 1.9.0 ----- @@ -577,29 +662,28 @@ default behaviour to automatically load PYTHONSTARTUP and ~/.pythonrc.py unless --no-startup is set. Changes: - - Fix: pipchecker, fix up-to-date check for Github sha commits - - Fix: JSONField, fix handling to_python() for strings with tests - - Fix: print_settings, fix print_settings to receive positional args - - Improvement: shell_plus, update PYTHONSTARTUP / pythonrc handling to match Django - - Improvement: shell_plus, added new 1.11 features from django.db.models to shell_plus import list - - Improvement: runserver_plus, startup message now accounts for https - - Docs: jobs, improve documentation about jobs scheduling - - Docs: admin, add documentation for ForeignKeyAutocompleteStackedInline and ForeignKeyAutocompleteTabularInline - - Docs: fix typos +- Fix: pipchecker, fix up-to-date check for Github sha commits +- Fix: JSONField, fix handling to_python() for strings with tests +- Fix: print_settings, fix print_settings to receive positional args +- Improvement: shell_plus, update PYTHONSTARTUP / pythonrc handling to match Django +- Improvement: shell_plus, added new 1.11 features from django.db.models to shell_plus import list +- Improvement: runserver_plus, startup message now accounts for https +- Docs: jobs, improve documentation about jobs scheduling +- Docs: admin, add documentation for ForeignKeyAutocompleteStackedInline and ForeignKeyAutocompleteTabularInline +- Docs: fix typos 1.8.1 ----- Changes: - - Build: use tox's 'TOXENV' environment variable - - Fix: resetdb, fix problem that 'utf8_support' option is ignored - - Improvement: export_emails, moved custom csv UnicodeWriter (for py2) into compat.py - - Translations: pt, removed since it was causing issues with the builds - if anybody wants to update and fix it that would be - much appreciated ! - +- Build: use tox's 'TOXENV' environment variable +- Fix: resetdb, fix problem that 'utf8_support' option is ignored +- Improvement: export_emails, moved custom csv UnicodeWriter (for py2) into compat.py +- Translations: pt, removed since it was causing issues with the builds + if anybody wants to update and fix it that would be + much appreciated ! 1.8.0 ----- @@ -610,120 +694,119 @@ Deprecation schedule for JSONField has been removed after requests from the community. Changes: - - Fix: runserver_plus, fixed Python 3 print syntax - - Fix: sqldiff, Use 'display_size', not 'precision' to identify MySQL bool field - - Fix: export_emails, fix and refactor the command and all its output options - - Improvement: tests, added Python 3.6 and PyPy3.5-5.8.0 - - Improvement: clear_cache, add --cache option to support multiple caches - - Improvement: runserver_plus, limit printing SQL queries to avoid flooding the terminal - - Improvement: shell_plus, limit printing SQL queries to avoid flooding the terminal - - Docs: graph_models, document including/excluding specific models - - Docs: shell_plus, added PTPython +- Fix: runserver_plus, fixed Python 3 print syntax +- Fix: sqldiff, Use 'display_size', not 'precision' to identify MySQL bool field +- Fix: export_emails, fix and refactor the command and all its output options +- Improvement: tests, added Python 3.6 and PyPy3.5-5.8.0 +- Improvement: clear_cache, add --cache option to support multiple caches +- Improvement: runserver_plus, limit printing SQL queries to avoid flooding the terminal +- Improvement: shell_plus, limit printing SQL queries to avoid flooding the terminal +- Docs: graph_models, document including/excluding specific models +- Docs: shell_plus, added PTPython 1.7.9 ----- Changes: - - Fix: AutoSlugField, foreignkey relationships - - Fix: shell_plus, supported backends 'postgresql' for set_application_name - - Improvement: various commands, Add syntax highlighting when printing SQL - - Improvement: pipchecker, treat rc versions as unstable - - Improvement: shell_plus, allow to subclass and overwrite import_objects - - Improvement: shell_plus, fix SHELL_PLUS_PRE_IMPORTS example - - Improvement: setup.py, fix and unify running tests - - Improvement: runserver_plus, add RUNSERVERPLUS_POLLER_RELOADER_TYPE setting - - Improvement: generate_secret_key, use algorithm from django - - Docs: fix grammar and spelling mistakes - +- Fix: AutoSlugField, foreignkey relationships +- Fix: shell_plus, supported backends 'postgresql' for set_application_name +- Improvement: various commands, Add syntax highlighting when printing SQL +- Improvement: pipchecker, treat rc versions as unstable +- Improvement: shell_plus, allow to subclass and overwrite import_objects +- Improvement: shell_plus, fix SHELL_PLUS_PRE_IMPORTS example +- Improvement: setup.py, fix and unify running tests +- Improvement: runserver_plus, add RUNSERVERPLUS_POLLER_RELOADER_TYPE setting +- Improvement: generate_secret_key, use algorithm from django +- Docs: fix grammar and spelling mistakes 1.7.8 ----- Changes: - - Improvement: django 1.11, add testing for Django 1.11 - - Improvement: pipchecker, make it possible to parse https github urls - - Improvement: unreferenced_files, make command much faster by using set() - - Docs: add undocumented commands - - Docs: shell_plus, additional documentation for referencing nested modules - - Fix: sync_s3, fix exclusion of directories - - Fix: runprofileserver, fix ip:port specification - - Fix: runprofileserver, support --nothreading +- Improvement: django 1.11, add testing for Django 1.11 +- Improvement: pipchecker, make it possible to parse https github urls +- Improvement: unreferenced_files, make command much faster by using set() +- Docs: add undocumented commands +- Docs: shell_plus, additional documentation for referencing nested modules +- Fix: sync_s3, fix exclusion of directories +- Fix: runprofileserver, fix ip:port specification +- Fix: runprofileserver, support --nothreading 1.7.7 ----- Changes: - - Improvement: admin_generator, use decorator style for registering ModelAdmins. - - Improvement: sqldiff, quote tablename for PRAGMA in sqlite - - Fix: graph_models, Fix `attributes` referenced before assignment - - Fix: pipchecker, Fix AttributeError caused by missing method +- Improvement: admin_generator, use decorator style for registering ModelAdmins. +- Improvement: sqldiff, quote tablename for PRAGMA in sqlite +- Fix: graph_models, Fix `attributes` referenced before assignment +- Fix: pipchecker, Fix AttributeError caused by missing method 1.7.6 ----- Changes: - - Improvement: sqldiff, ignore proxy models in diff (with cli option to include them if wanted) +- Improvement: sqldiff, ignore proxy models in diff (with cli option to include them if wanted) 1.7.5 ----- Changes: - - New: ForeignKeyAutocompleteAdmin, Add autocomplete for inline model admins - - Improvement: graph_models, Rewrite modelviz module from method to class based processor - - Improvement: db fields, make MAX_UNIQUE_QUERY_ATTEMPTS configurable per field and via settings - - Improvement: runserver_plus, Added nopin option to disable pin - - Fix: graph_models, Support PyDot 1.2.0 and higher - - Fix: shell_plus, Fix that aliases from SHELL_PLUS_MODEL_ALIASES were not applied - - Removed: validate_templatetags, remove support for pre django-1.5 style {% url %} tags - - Cleanup: removing support for end-of-life Python 3.2 - - Docs: simplify installation instructions - - Docs: fix example for NOTEBOOK_ARGUMENTS - - Docs: Remove extraneous '}' characters from shell_plus docs +- New: ForeignKeyAutocompleteAdmin, Add autocomplete for inline model admins +- Improvement: graph_models, Rewrite modelviz module from method to class based processor +- Improvement: db fields, make MAX_UNIQUE_QUERY_ATTEMPTS configurable per field and via settings +- Improvement: runserver_plus, Added nopin option to disable pin +- Fix: graph_models, Support PyDot 1.2.0 and higher +- Fix: shell_plus, Fix that aliases from SHELL_PLUS_MODEL_ALIASES were not applied +- Removed: validate_templatetags, remove support for pre django-1.5 style {% url %} tags +- Cleanup: removing support for end-of-life Python 3.2 +- Docs: simplify installation instructions +- Docs: fix example for NOTEBOOK_ARGUMENTS +- Docs: Remove extraneous '}' characters from shell_plus docs 1.7.4 ----- Changes: - - Improvement: show_urls, support --no-color option - - Fix: notes, Fix reading templates setting after django 1.8 - - Fix: create_app, Fixed typo in deprecation warning - - Fix: shell_plus, Use new location for url reverse import - - Docs: some commands where missing from the docs - - Docs: runscript, added documentation for --traceback +- Improvement: show_urls, support --no-color option +- Fix: notes, Fix reading templates setting after django 1.8 +- Fix: create_app, Fixed typo in deprecation warning +- Fix: shell_plus, Use new location for url reverse import +- Docs: some commands where missing from the docs +- Docs: runscript, added documentation for --traceback 1.7.3 ----- Changes: - - Fix: ForeignKeySearchInput, fix bug with constructing search_path urls - - Docs: runscript, fix runscript example - - Deprecation: JSONField, Django now includes JSONField our field is now deprecated +- Fix: ForeignKeySearchInput, fix bug with constructing search_path urls +- Docs: runscript, fix runscript example +- Deprecation: JSONField, Django now includes JSONField our field is now deprecated 1.7.2 ----- Changes: - - Fix: passwd, Update passwd command up to Django>=1.8 - - Improvement: shell_plus, add settings.SHELL_PLUS_PRINT_SQL config option - - Improvement: shell_plus, allow to specifies the connection file to use if using the --kernel option +- Fix: passwd, Update passwd command up to Django>=1.8 +- Improvement: shell_plus, add settings.SHELL_PLUS_PRINT_SQL config option +- Improvement: shell_plus, allow to specifies the connection file to use if using the --kernel option 1.7.1 ----- Changes: - - Fix: sqldiff, fix optional app_label arguments - - Fix: runscript, remove args from command class - - Doc: runscript, remove = from --script-args example +- Fix: sqldiff, fix optional app_label arguments +- Fix: runscript, remove args from command class +- Doc: runscript, remove = from --script-args example 1.7.0 ----- @@ -736,70 +819,70 @@ make sure that Django Extensions uses the current Django API's. This should result in better and easier to maintain code (and hopefully less bugs :). This release touches a lot of code if you have any issues please report them -at https://github.com/django-extensions/django-extensions/issues +at [https://github.com/django-extensions/django-extensions/issues] We still need more tests to make sure we don't break people's projects when refactoring. If you have some spare time please contribute tests ! Changes: - - Cleanup: removing backwards compatibility hacks for (now) unsupported versions of Django - - Cleanup: use six instead of home grown functions - - Fix: AutoSlugField, allow_duplicates didn't set slug value to model instance - - Fix: MongoDB fields, verbose_name on mongoengine fields does not seem to be supported - - Fix: MongoDB fields, fix relative import problem with json.py - - Improvement: Start using pre-commit - - Improvement: syncdata, Replace custom transaction logic with transaction.atomic - - Improvement: Django 1.10, use from_db_value instead of models.SubfieldBase - - Improvement: print_user_session, support for non standard user model - - Improvement: widont, tests to work with py2 and py3 - - Improvement: runserver_plus, prevent 2nd reload of debugger on runserver_plus - - Improvement: runserver_plus, prevent killing the server when request.META values are evaluated - - Improvement: reset_db, add argument to make closing sessions optional - - Improvement: print_settings, Fix positional arguments - - Improvement: runscript, migrate to argparse and add_arguments - - Improvement: graph_models, do not rely on .models_module for inclusion in output - - Improvement: jsonfield, fix issues with mutable default - - Docs: Convert readthedocs links for their .org -> .io migration +- Cleanup: removing backwards compatibility hacks for (now) unsupported versions of Django +- Cleanup: use six instead of home grown functions +- Fix: AutoSlugField, allow_duplicates didn't set slug value to model instance +- Fix: MongoDB fields, verbose_name on mongoengine fields does not seem to be supported +- Fix: MongoDB fields, fix relative import problem with json.py +- Improvement: Start using pre-commit +- Improvement: syncdata, Replace custom transaction logic with transaction.atomic +- Improvement: Django 1.10, use from_db_value instead of models.SubfieldBase +- Improvement: print_user_session, support for non standard user model +- Improvement: widont, tests to work with py2 and py3 +- Improvement: runserver_plus, prevent 2nd reload of debugger on runserver_plus +- Improvement: runserver_plus, prevent killing the server when request.META values are evaluated +- Improvement: reset_db, add argument to make closing sessions optional +- Improvement: print_settings, Fix positional arguments +- Improvement: runscript, migrate to argparse and add_arguments +- Improvement: graph_models, do not rely on .models_module for inclusion in output +- Improvement: jsonfield, fix issues with mutable default +- Docs: Convert readthedocs links for their .org -> .io migration 1.6.7 ----- Changes: - - Fix: describe_form, fix No module named 'django.db.models.loading' error - - Improvement: shell_plus, Add a setting to prefix all models in an application #887 - - Improvement: pipchecker, check for requirements-{dev,prod}.txt as well - - Docs: pipchecker, update documentation + +- Fix: describe_form, fix No module named 'django.db.models.loading' error +- Improvement: shell_plus, Add a setting to prefix all models in an application #887 +- Improvement: pipchecker, check for requirements-{dev,prod}.txt as well +- Docs: pipchecker, update documentation 1.6.6 ----- Changes: - - Fix: admin_generator, fix for using all apps in Django <1.7 - - Fix: dump_script, fix for using all apps in Django <1.7 - - Fix: UniqueFieldMixin, resolve get_fields_with_model deprecation Django 1.10 - - Fix: runprofileserver, Fix call grind format to enable source code navigation in qcachegrind. - - Docs: runserver_plus, add a little note about the debugger PIN. +- Fix: admin_generator, fix for using all apps in Django <1.7 +- Fix: dump_script, fix for using all apps in Django <1.7 +- Fix: UniqueFieldMixin, resolve get_fields_with_model deprecation Django 1.10 +- Fix: runprofileserver, Fix call grind format to enable source code navigation in qcachegrind. +- Docs: runserver_plus, add a little note about the debugger PIN. 1.6.5 ----- Bumped version number since PyPi returns 500 errors while uploading packages :( - 1.6.4 ----- Changes: - - Fix: jobs cache_cleanup, use `caches` instead of deprecated `get_cache` - - Fix: ModificationDateTimeField, missing default value for `update_modified` - - Fix: modelviz, use get_model_compat and look up missing app_label - - Fix: modelviz, use get_models_for_app instead of get_models_compat - - Fix: dumpscript, use `list_app_labels` instead of `get_apps` when no app_labels are given - - Improvement: compat.py, move code from try to else block for Django 1.7+ - - Docstring: get_models_for_app, clearify argument +- Fix: jobs cache_cleanup, use `caches` instead of deprecated `get_cache` +- Fix: ModificationDateTimeField, missing default value for `update_modified` +- Fix: modelviz, use get_model_compat and look up missing app_label +- Fix: modelviz, use get_models_for_app instead of get_models_compat +- Fix: dumpscript, use `list_app_labels` instead of `get_apps` when no app_labels are given +- Improvement: compat.py, move code from try to else block for Django 1.7+ +- Docstring: get_models_for_app, clearify argument 1.6.3 ----- @@ -812,439 +895,441 @@ Bumped version number for incomplete PyPi upload The long over due release :-) Changes: - - Fix: JsonFields, do not parse floats as decimals. This fixes bugs that causes - them to be returned as strings after multiple saves. Note that this can - be backwards incompatible ! - - Fix: use add_arguments() instead of option_list (Django 1.10) - - Fix: create_command, Django 1.9 fixes - - Fix: create_jobs, Django 1.9 fixes - - Fix: RandomCharField, when not unique get the first value from the generator - - Fix: graph_models, render() must be called with a dict - - Fix: graph_models, use force_bytes fixes command for Python 3 - - Fix: graph_models, fix django 1.6 compatibility for strings defined relation - - Fix: graph_models, fix settings.GRAPH_MODELS breaking the command - - Fix: graph_models, add support for lazy relationships - - Fix: ForeignKeyAutocompleteAdmin, url_patterns is just a list (Django 1.9+) - - Fix: ForeignKeySearchInput, use url reversing instead of hardcoded paths - - Fix: find_template, Fix for Django 1.8+ - - Fix: admin_generator, incompatible "default" identifier raising TypeError - - Improvement: show_urls, add json and pretty-json formatting - - Improvement: runserver_plus, add support for whitenoise - - Improvement: ModificationDateTimeField, add parameter to preserve timestamps on save - - Improvement: runprofileserver, raise command error when hotspot is not available - - Improvement: reset_db, better parsing of mysql cnf file - - Improvement: restored coverage for Python 3.2 - - Improvement: pep8 fixes, remove unused shims & imports & commented code - - Improvement: graph_models, JSON output - - Improvement: graph_models, add wildcard filters - - Docs: removed text on donations, the hope was that we could generate some - funds to have more consistent development and outreach. - - Docs: runserver_plus, added some documentation about LOGGING - - Docs: runscript, update documentation to match Django tutorial for Django 1.8+ - - Docs: runprofileserver, add documentation on profiler choices - - Docs: update_permissions, add basic documentation for command +- Fix: JsonFields, do not parse floats as decimals. This fixes bugs that causes + them to be returned as strings after multiple saves. Note that this can + be backwards incompatible ! +- Fix: use add_arguments() instead of option_list (Django 1.10) +- Fix: create_command, Django 1.9 fixes +- Fix: create_jobs, Django 1.9 fixes +- Fix: RandomCharField, when not unique get the first value from the generator +- Fix: graph_models, render() must be called with a dict +- Fix: graph_models, use force_bytes fixes command for Python 3 +- Fix: graph_models, fix django 1.6 compatibility for strings defined relation +- Fix: graph_models, fix settings.GRAPH_MODELS breaking the command +- Fix: graph_models, add support for lazy relationships +- Fix: ForeignKeyAutocompleteAdmin, url_patterns is just a list (Django 1.9+) +- Fix: ForeignKeySearchInput, use url reversing instead of hardcoded paths +- Fix: find_template, Fix for Django 1.8+ +- Fix: admin_generator, incompatible "default" identifier raising TypeError +- Improvement: show_urls, add json and pretty-json formatting +- Improvement: runserver_plus, add support for whitenoise +- Improvement: ModificationDateTimeField, add parameter to preserve timestamps on save +- Improvement: runprofileserver, raise command error when hotspot is not available +- Improvement: reset_db, better parsing of mysql cnf file +- Improvement: restored coverage for Python 3.2 +- Improvement: pep8 fixes, remove unused shims & imports & commented code +- Improvement: graph_models, JSON output +- Improvement: graph_models, add wildcard filters +- Docs: removed text on donations, the hope was that we could generate some + funds to have more consistent development and outreach. +- Docs: runserver_plus, added some documentation about LOGGING +- Docs: runscript, update documentation to match Django tutorial for Django 1.8+ +- Docs: runprofileserver, add documentation on profiler choices +- Docs: update_permissions, add basic documentation for command 1.6.1 ----- Changes: - - Revert: JSONField, revert Django 1.9 fix as it breaks the field (ticket #781) +- Revert: JSONField, revert Django 1.9 fix as it breaks the field (ticket #781) 1.6.0 ----- Changes: - - Fix: Django 1.9 compatibility - - New: runserver_plus, add --startup-messages to control when to show them - - New: added support for Python 3.5 - - Improvement: show_template_tags, renamed from show_templatetags for consistency - - Removed: jquery library (after dropping support for Django 1.5) +- Fix: Django 1.9 compatibility +- New: runserver_plus, add --startup-messages to control when to show them +- New: added support for Python 3.5 +- Improvement: show_template_tags, renamed from show_templatetags for consistency +- Removed: jquery library (after dropping support for Django 1.5) 1.5.9 ----- Changes: - - Fix: wheel still had the old migrations directory in the package +- Fix: wheel still had the old migrations directory in the package 1.5.8 ----- Changes: - - Fix: migrations, fix BadMigrationError with Django 1.8+ - - Fix: reset_db, Django 1.8+ compatibility fix - - Fix: runserver_plus, fix signature of null_technical_500_response for Django 1.8+ - - Fix: graph_models, use force_bytes instead of .decode('utf8') - - Improvement: print_settings, add format option to only print values - - Improvement: print_settings, add format option for simple key = value text output - - Improvement: email_export, documentation updates - - Improvement: shell_plus, auto load conditional db expressions Case and When +- Fix: migrations, fix BadMigrationError with Django 1.8+ +- Fix: reset_db, Django 1.8+ compatibility fix +- Fix: runserver_plus, fix signature of null_technical_500_response for Django 1.8+ +- Fix: graph_models, use force_bytes instead of .decode('utf8') +- Improvement: print_settings, add format option to only print values +- Improvement: print_settings, add format option for simple key = value text output +- Improvement: email_export, documentation updates +- Improvement: shell_plus, auto load conditional db expressions Case and When 1.5.7 ----- Changes: - - Fix: CreationDateTimeField, migration error - - Fix: ModificationDateTimeField, migration error - - Fix: shell_plus, options is not always in db config dictionary - - Fix: admin filters, contrib.admin.util fallback code - - Fix: graph_models, correctly support parsing lists for cli options - - Improvement: sqldsn, support postfix - - Improvement: utils, remove get_project_root function +- Fix: CreationDateTimeField, migration error +- Fix: ModificationDateTimeField, migration error +- Fix: shell_plus, options is not always in db config dictionary +- Fix: admin filters, contrib.admin.util fallback code +- Fix: graph_models, correctly support parsing lists for cli options +- Improvement: sqldsn, support postfix +- Improvement: utils, remove get_project_root function 1.5.6 ----- Changes: - - New: RandomCharField, prepopulates random character string - - New: (Not)NullFieldListFilter, filters for admin - - New: runserver_plus, integrate with django-pdb - - New: runserver_plus, add check_migrations from Django - - Improvement: show_urls, nested namespace support - - Improvement: show_urls, allow to specify alternative urlconf - - Improvement: show_urls, support i18n_patterns - - Improvement: show_urls, use --language to filter on a particular language - - Improvement: admin_generator, added docstrings to module - - Improvement: shell_plus, allow cli arguments to be passed to ipython - - Improvement: shell_plus, fixed PYTHONPATH bug when using django-admin shell_plus --notebook - - Improvement: shell_plus, set application_name on PostgreSQL databases - - Improvement: shell_plus, load user pypython config file - - Improvement: CreationDateTimeField, use auto_now_add instead of default ModificationDateTimeField - - Improvement: ModificationDateTimeField, use auto_now instead of pre_save method - - Improvement: ForeignKeyAutocompleteAdmin, added ability to filter autocomplete query - - Fix: shell_plus, support for pypython>=0.27 - - Fix: shell_plus, load apps and models directly through the apps interface when available - - Fix: shell_plus, use ipython start_ipython instead of embed - - Fix: shell_plus, fix swalling ImportErrors with IPython 3 and higher - - Fix: dumpscript, fix missing imports in dumped script - - Fix: admin_generator, fix issues with Django 1.9 - - Fix: template tags, move exception for import failure to inside of the template tags - - Fix: reset_db, fix for Django 1.9 - - Fix: runserver_plus, fix for Django 1.9 +- New: RandomCharField, prepopulates random character string +- New: (Not)NullFieldListFilter, filters for admin +- New: runserver_plus, integrate with django-pdb +- New: runserver_plus, add check_migrations from Django +- Improvement: show_urls, nested namespace support +- Improvement: show_urls, allow to specify alternative urlconf +- Improvement: show_urls, support i18n_patterns +- Improvement: show_urls, use --language to filter on a particular language +- Improvement: admin_generator, added docstrings to module +- Improvement: shell_plus, allow cli arguments to be passed to ipython +- Improvement: shell_plus, fixed PYTHONPATH bug when using django-admin shell_plus --notebook +- Improvement: shell_plus, set application_name on PostgreSQL databases +- Improvement: shell_plus, load user pypython config file +- Improvement: CreationDateTimeField, use auto_now_add instead of default ModificationDateTimeField +- Improvement: ModificationDateTimeField, use auto_now instead of pre_save method +- Improvement: ForeignKeyAutocompleteAdmin, added ability to filter autocomplete query +- Fix: shell_plus, support for pypython>=0.27 +- Fix: shell_plus, load apps and models directly through the apps interface when available +- Fix: shell_plus, use ipython start_ipython instead of embed +- Fix: shell_plus, fix swalling ImportErrors with IPython 3 and higher +- Fix: dumpscript, fix missing imports in dumped script +- Fix: admin_generator, fix issues with Django 1.9 +- Fix: template tags, move exception for import failure to inside of the template tags +- Fix: reset_db, fix for Django 1.9 +- Fix: runserver_plus, fix for Django 1.9 1.5.5 ----- Changes: - - Fix: sqldiff, previous Django 1.8 fix was slightly broken +- Fix: sqldiff, previous Django 1.8 fix was slightly broken 1.5.4 ----- Changes: - - Improvement: syncdata, add skip-remove option - - Improvement: logging, report how often mail was ratelimited - - Fix: admin, Django 1.8 compatibility module_name is now called model_name - - Fix: notes, Python 3.x fix force output of filter into list - - Fix: sqldiff, fix for Django 1.8 +- Improvement: syncdata, add skip-remove option +- Improvement: logging, report how often mail was ratelimited +- Fix: admin, Django 1.8 compatibility module_name is now called model_name +- Fix: notes, Python 3.x fix force output of filter into list +- Fix: sqldiff, fix for Django 1.8 1.5.3 ----- Changes: - - New: ratelimiter, a simple ratelimiter filter for Python logging - - Fix: various improvements for Django 1.8 - - Fix: sync_s3, use os.walk instead of os.path.walk (py3 fix) - - Improvement: pipchecker, use name instead of url_name to fix casing mismatches - - Improvement: pipchecker, use https - - Improvement: pipchecker, fix issues with new(er) pip versions - - Docs: fixed a few typos - - Docs: added documentation about NOTEBOOK_ARGUMENTS settings +- New: ratelimiter, a simple ratelimiter filter for Python logging +- Fix: various improvements for Django 1.8 +- Fix: sync_s3, use os.walk instead of os.path.walk (py3 fix) +- Improvement: pipchecker, use name instead of url_name to fix casing mismatches +- Improvement: pipchecker, use https +- Improvement: pipchecker, fix issues with new(er) pip versions +- Docs: fixed a few typos +- Docs: added documentation about NOTEBOOK_ARGUMENTS settings 1.5.2 ----- Changes: - - New: sqldsn, prints Data Source Name for defined database(s) - - Fix: graph_models, Django 1.8 support - - Fix: highlighting tag, fix usage of is_safe - - Fix: runscript, fix for runscript with AppConfig apps - - Fix: sqldiff, KeyError when index is missing in database - - Fix: sqldiff, multi column indexes was also counted as a single colomn index - - Improvements: JSONField, Added try/catch for importing json/simplejson for Django 1.7 +- New: sqldsn, prints Data Source Name for defined database(s) +- Fix: graph_models, Django 1.8 support +- Fix: highlighting tag, fix usage of is_safe +- Fix: runscript, fix for runscript with AppConfig apps +- Fix: sqldiff, KeyError when index is missing in database +- Fix: sqldiff, multi column indexes was also counted as a single colomn index +- Improvements: JSONField, Added try/catch for importing json/simplejson for Django 1.7 1.5.1 ----- Changes: - - New: runserver_plus, add support for --extra-files parameter - - Fix: Django 1.7 defined MIDDLEWARE_CLASSES for tests - - Fix: shell_plus, problem when auto-loading modules with empty '__module__' property - - Improvement: shell_plus, IPython 3.x support for notebooks - - Improvement: tests, move to py.test and lots of other improvements - - Improvement: create_app, add migrations folder - - Improvement: tox.ini, refactored to be more DRY - - Improvement: runserver_plus, also reload on changes to translation files - - Improvement: runserver_plus, add reloader_interval support - - Improvement: create_template_tags, removed unused command line option - - Docs: print_user_for_session, add note about SESSION_ENGINE - - Docs: runserver_plus, added section about IO calls and CPU usage +- New: runserver_plus, add support for --extra-files parameter +- Fix: Django 1.7 defined MIDDLEWARE_CLASSES for tests +- Fix: shell_plus, problem when auto-loading modules with empty '__module__' property +- Improvement: shell_plus, IPython 3.x support for notebooks +- Improvement: tests, move to py.test and lots of other improvements +- Improvement: create_app, add migrations folder +- Improvement: tox.ini, refactored to be more DRY +- Improvement: runserver_plus, also reload on changes to translation files +- Improvement: runserver_plus, add reloader_interval support +- Improvement: create_template_tags, removed unused command line option +- Docs: print_user_for_session, add note about SESSION_ENGINE +- Docs: runserver_plus, added section about IO calls and CPU usage 1.5.0 ----- Changes: - - Fix: various fixes for Django 1.8 - - Improvement: shell_plus, autodetect vi mode by looking at $EDITOR shell env setting - - Improvement: shell_plus, print which shell is being used at verbosity > 1 - - Improvement: shell_plus, added --no-browser option for IPython notebooks - - Improvement: tox.ini, updated to latest Django versions - - Docs: add reference to JSONField in documentation - - Docs: fixed various typo's and links in docs and changelog - - Docs: added some basic use cases to README - - Docs: added information for companies or people wanting to donate towards the project - - Fix: graphmodels, fix for python3 - - Fix: dumpscript, fix check for missing import_helper module in Python 3 - - Fix: runprofileserver, explicitly close file to avoid error on windows - - Fix: json field, migration issues when adding new JSONField to existing model - - Fix: runjobs, fix python3 issues +- Fix: various fixes for Django 1.8 +- Improvement: shell_plus, autodetect vi mode by looking at $EDITOR shell env setting +- Improvement: shell_plus, print which shell is being used at verbosity > 1 +- Improvement: shell_plus, added --no-browser option for IPython notebooks +- Improvement: tox.ini, updated to latest Django versions +- Docs: add reference to JSONField in documentation +- Docs: fixed various typo's and links in docs and changelog +- Docs: added some basic use cases to README +- Docs: added information for companies or people wanting to donate towards the project +- Fix: graphmodels, fix for python3 +- Fix: dumpscript, fix check for missing import_helper module in Python 3 +- Fix: runprofileserver, explicitly close file to avoid error on windows +- Fix: json field, migration issues when adding new JSONField to existing model +- Fix: runjobs, fix python3 issues 1.4.9 ----- Changes: - - New: drop_test_database, drops the test database - - New: command_signals, git commit -a -m 'bumped version number' (see docs) - - Bugfix: runserver_plus, removed empty lines when logging on Python 3 +- New: drop_test_database, drops the test database +- New: command_signals, git commit -a -m 'bumped version number' (see docs) +- Bugfix: runserver_plus, removed empty lines when logging on Python 3 1.4.8 ----- Changes: - - Bugfix: validators, fix NoWhitespaceValidator __eq__ check +- Bugfix: validators, fix NoWhitespaceValidator __eq__ check 1.4.7 ----- Changes: - - New: validators.py, NoControlCharactersValidator and NoWhitespaceValidator - - New: EmailNotificationCommand class, email exceptions from Django Commands - - Improvement: runserver_plus, enable threading by default and added --nothreading - - Improvement: runscript, better detection when import error occured in script - - Improvement: runscript, use EmailNotificationCommand class - - Deprecation: deprecated UUIDField since Django 1.8 will have a native version. - - Removed: completely remove support for automatically finding project root. +- New: validators.py, NoControlCharactersValidator and NoWhitespaceValidator +- New: EmailNotificationCommand class, email exceptions from Django Commands +- Improvement: runserver_plus, enable threading by default and added --nothreading +- Improvement: runscript, better detection when import error occured in script +- Improvement: runscript, use EmailNotificationCommand class +- Deprecation: deprecated UUIDField since Django 1.8 will have a native version. +- Removed: completely remove support for automatically finding project root. 1.4.6 ----- Changes: - - Improvement: sqldiff, fix for dbcolumn not used in few places when generating the sqldiff - - Fix: sqldiff, backwards compatibility fix for Django 1.4 - - Fix: ForeignKey Field, handling of __str__ instead of __unicode__ in python3 +- Improvement: sqldiff, fix for dbcolumn not used in few places when generating the sqldiff +- Fix: sqldiff, backwards compatibility fix for Django 1.4 +- Fix: ForeignKey Field, handling of __str__ instead of __unicode__ in python3 1.4.5 ----- Changes: - - New: clear_cache, Clear django cache, useful when testing or deploying - - Improvement: AutoSlugField, add the possibility to define a custom slugify function - - Improvement: shell_plus --notebook, add a big warning when the notebook extension is not going to be loaded - - Improvement: setup.py, add pypy classifier - - Improvement: readme, add pypy badges - - Fix: admin_generator, Fixed Python 3 __unicode__/__str__ compatibility +- New: clear_cache, Clear django cache, useful when testing or deploying +- Improvement: AutoSlugField, add the possibility to define a custom slugify function +- Improvement: shell_plus --notebook, add a big warning when the notebook extension is not going to be loaded +- Improvement: setup.py, add pypy classifier +- Improvement: readme, add pypy badges +- Fix: admin_generator, Fixed Python 3 __unicode__/__str__ compatibility 1.4.4 ----- Changes: - - Fix: admin_generator, fix ImproperlyConfigured exception on Django 1.7 - - Improvement: Remove "requires_model_validation" and "requires_system_checks" in commands which set the default value +- Fix: admin_generator, fix ImproperlyConfigured exception on Django 1.7 +- Improvement: Remove "requires_model_validation" and "requires_system_checks" in commands which set the default value 1.4.1 ----- Changes: - - New: shell_plus, Added python-prompt-toolkit integration for shell_plus - - New: shell_plus, Added --ptipython (PYPython + IPython) - - Improvement: reset_db, output traceback to easy debugging in case of error - - Improvement: dumpscript, add --autofield to dumpscript to include autofields in export - - Improvement: show_urls, Include namespace in URL name - - Improvement: show_urls, Allow multiple decorators on the show_urls command - - Improvement: runscript, show script errors with verbosity > 1 - - Fix: jobs, daily_cleanup job use clearsessions for Django 1.5 and later - - Fix: shell_plus, refactored importing and selecting shells to avoid polluted exception - - Fix: shell_plus, Fix model loading for sentry +- New: shell_plus, Added python-prompt-toolkit integration for shell_plus +- New: shell_plus, Added --ptipython (PYPython + IPython) +- Improvement: reset_db, output traceback to easy debugging in case of error +- Improvement: dumpscript, add --autofield to dumpscript to include autofields in export +- Improvement: show_urls, Include namespace in URL name +- Improvement: show_urls, Allow multiple decorators on the show_urls command +- Improvement: runscript, show script errors with verbosity > 1 +- Fix: jobs, daily_cleanup job use clearsessions for Django 1.5 and later +- Fix: shell_plus, refactored importing and selecting shells to avoid polluted exception +- Fix: shell_plus, Fix model loading for sentry 1.4.0 ----- Changes: - - New admin_generator, can generate a admin.py file from models - - Improvement: sqldiff, use the same exit codes as diff uses - - Improvement: sqldiff, add support for unsigned numeric fields - - Improvement: sqldiff, add NOT NULL support for MySQL - - Improvement: sqldiff, add proper AUTO_INCREMENT support for MySQL - - Improvement: sqldiff, detect tables for which no model exists - - Improvement: travis.yml, add pypy to tests - - Fix: sqldiff, fix for mysql misreported field lengths - - Fix: sqldiff, in PG custom int primary keys would be mistaking for serial - - Fix: sqldiff, use Django 1.7 db_parameters() for detect check constraints - - Fix: update_permissions, Django 1.7 support - - Fix: encrypted fields, fix for Django 1.7 migrations +- New admin_generator, can generate a admin.py file from models +- Improvement: sqldiff, use the same exit codes as diff uses +- Improvement: sqldiff, add support for unsigned numeric fields +- Improvement: sqldiff, add NOT NULL support for MySQL +- Improvement: sqldiff, add proper AUTO_INCREMENT support for MySQL +- Improvement: sqldiff, detect tables for which no model exists +- Improvement: travis.yml, add pypy to tests +- Fix: sqldiff, fix for mysql misreported field lengths +- Fix: sqldiff, in PG custom int primary keys would be mistaking for serial +- Fix: sqldiff, use Django 1.7 db_parameters() for detect check constraints +- Fix: update_permissions, Django 1.7 support +- Fix: encrypted fields, fix for Django 1.7 migrations 1.3.11 ------ Changes: - - Improvement: sqldiff, show differences for not managed tables - - Improvement: show_urls -f aligned, 3 spaces between columns - - Improvement: reset_db, support mysql options files in reset_db - - Fix: sqldiff, Fixed bug with --output_text option and notnull-differ text - - Fix: reset_db, Fix for PostgreSQL databases with dashes, dots, etc in the name - - Fix: dumpscript, AttributeError for datefields that are None - - Docs: Adding RUNSERVERPLUS_SERVER_ADDRESS_PORT to docs +- Improvement: sqldiff, show differences for not managed tables +- Improvement: show_urls -f aligned, 3 spaces between columns +- Improvement: reset_db, support mysql options files in reset_db +- Fix: sqldiff, Fixed bug with --output_text option and notnull-differ text +- Fix: reset_db, Fix for PostgreSQL databases with dashes, dots, etc in the name +- Fix: dumpscript, AttributeError for datefields that are None +- Docs: Adding RUNSERVERPLUS_SERVER_ADDRESS_PORT to docs 1.3.10 ------ Changes: - - Fix: show_urls, fix bug in new formatter when column is empty +- Fix: show_urls, fix bug in new formatter when column is empty 1.3.9 ----- Changes: - - Feature: shell_plus, add --kernel option to start as standalone IPython kernel - - Feature: reset_db, Programmatically determine PostGIS template - - Feature: sqldiff, add support for PointField and MultiPolygonField - - Test: renamed test app - - Fix: runserver_plus, --print-sql for Django 1.7 - - Fix: shell_plus, --print-sql for Django 1.7 - - Fix: show_urls, add support for functions that use functools.partial - - Fix: show_urls, add formatter for aligned output (will most likely become future default) - - Fix: shell_plus / notebook, support for Django 1.7 - - Docs: various fixes and improvements - - Cleanup: Remove work arounds for Django 0.96 and earlier +- Feature: shell_plus, add --kernel option to start as standalone IPython kernel +- Feature: reset_db, Programmatically determine PostGIS template +- Feature: sqldiff, add support for PointField and MultiPolygonField +- Test: renamed test app +- Fix: runserver_plus, --print-sql for Django 1.7 +- Fix: shell_plus, --print-sql for Django 1.7 +- Fix: show_urls, add support for functions that use functools.partial +- Fix: show_urls, add formatter for aligned output (will most likely become future default) +- Fix: shell_plus / notebook, support for Django 1.7 +- Docs: various fixes and improvements +- Cleanup: Remove work arounds for Django 0.96 and earlier 1.3.8 ----- Changes: - - Feature: show_urls, add option to specify dense or verbose output format - - Improvement: better support for django 1.7 migrations - - Improvement: better support for django's admin docs - - BugFix: runjob, job_name and app_name was swapped in error message - - Docs: Update link to chinese docs - - Python3: unreferenced_files, fix python3 compatibility - - Python3: pipchecker, fix python3 compatibility + +- Feature: show_urls, add option to specify dense or verbose output format +- Improvement: better support for django 1.7 migrations +- Improvement: better support for django's admin docs +- BugFix: runjob, job_name and app_name was swapped in error message +- Docs: Update link to chinese docs +- Python3: unreferenced_files, fix python3 compatibility +- Python3: pipchecker, fix python3 compatibility 1.3.7 ----- Changes: - - Reinstated: clean_pyc and compile_pyc commands, these now depends on BASE_DIR - in settings.py as per Django 1.6. We urge everybody to include a - BASE_DIR settings in their project file! auto-detecting the - project-root is now deprecated and will be removed in 1.4.0. - - I18N: Added russian locale - - Docs: runscript, Add section about passing arguments to scripts - - Python3: show_url, Fixed to AttributeError 'func_globals' - - Deprecated: clean_pyc, compile_pyc, Auto-detecting project root +- Reinstated: clean_pyc and compile_pyc commands, these now depends on BASE_DIR + in settings.py as per Django 1.6. We urge everybody to include a + BASE_DIR settings in their project file! auto-detecting the + project-root is now deprecated and will be removed in 1.4.0. +- I18N: Added russian locale +- Docs: runscript, Add section about passing arguments to scripts +- Python3: show_url, Fixed to AttributeError 'func_globals' +- Deprecated: clean_pyc, compile_pyc, Auto-detecting project root 1.3.6 ----- Changes: - - Additional version bump because we mistakenly already uploaded - version 1.3.5 of the wheel package with the code of 1.3.4 +- Additional version bump because we mistakenly already uploaded + version 1.3.5 of the wheel package with the code of 1.3.4 1.3.5 ----- Changes: - - Feature: Django-Extensions is now also distributed as a Wheel package - - Improvement: dumpscript, improved the readability of comments in generated script - - Improvement: sqldiff, backported get_constraints() for PostgreSQL - - Improvement: shell_plus, consistent colorization - - BugFix: encrypted fields, there is no decoding to unicode in Python 3 - - BugFix: shell_plus, importing modules failed in some edge cases - - Django 1.7: included Django 1.7 in test suite - - Python 3.4: included Python 3.4 in test suite +- Feature: Django-Extensions is now also distributed as a Wheel package +- Improvement: dumpscript, improved the readability of comments in generated script +- Improvement: sqldiff, backported get_constraints() for PostgreSQL +- Improvement: shell_plus, consistent colorization +- BugFix: encrypted fields, there is no decoding to unicode in Python 3 +- BugFix: shell_plus, importing modules failed in some edge cases +- Django 1.7: included Django 1.7 in test suite +- Python 3.4: included Python 3.4 in test suite 1.3.4 ----- Changes: - - Feature: Start maintaining a CHANGELOG file in the repository - - Feature: ActivatorModelManager now has an ActivatorQuerySet - - Feature: Add a deconstruct() method for future Django 1.7 migration compatibility - - Feature: show_urls, now support --language for i18n_patterns - - Feature: show_urls, now shows the decoraters set on a view function - - Feature: graph_models, now support --include-models to restrict the graph to specified models - - Feature: print_settings, allow to specify the settings you want to see - - Improvement: graph_models, use '//' instead of '#' as comment character in dot files - - Improvement: graph_models, added error message for abstract models without explicit relations - - Improvement: JSONField, use python's built-in json support by default with fallback on django.utils.simplejson - - Improvement: PostgreSQLUUIDField, parse value into UUID type before sending it to the database - - Improvement: Use django.JQuery in autocomplete.js if available - - Improvement: use "a not in b" instead of "not a in b" in the entire codebase - - Removed: clean_pyc command since it does not work correctly in many cases - - Removed: sync_media_s3 command in favor of sync_s3 - - BugFix: syncdata, use pk instead of id for identifying primary key of objects - - BugFix: sync_s3, use safer content type per default - - BugFix: export_emails, filtering on groups - - BugFix: print_user_for_session, use USERNAME_FIELD if defined - - BugFix: update_permission, fixed TypeError issue - - BugFix: JSONField, do not coerse a json string into a python list - - BugFix: import json issue by using absolute imports - - BugFix: add minimal version number to six (>=1.2) - - Docs: graph_models, Added some documentation about using dot templates - - Docs: reset_db, short description on SQL DDL used - - Docs: Added specific list of supported Python and Django versions - - Docs: Add link to GoDjango screencast - - Docs: Add ShortUUIDField to docs - - Python3: fixes to graph_models and export_emails for Python3 compatibility +- Feature: Start maintaining a CHANGELOG file in the repository +- Feature: ActivatorModelManager now has an ActivatorQuerySet +- Feature: Add a deconstruct() method for future Django 1.7 migration compatibility +- Feature: show_urls, now support --language for i18n_patterns +- Feature: show_urls, now shows the decoraters set on a view function +- Feature: graph_models, now support --include-models to restrict the graph to specified models +- Feature: print_settings, allow to specify the settings you want to see +- Improvement: graph_models, use '//' instead of '#' as comment character in dot files +- Improvement: graph_models, added error message for abstract models without explicit relations +- Improvement: JSONField, use python's built-in json support by default with fallback on django.utils.simplejson +- Improvement: PostgreSQLUUIDField, parse value into UUID type before sending it to the database +- Improvement: Use django.JQuery in autocomplete.js if available +- Improvement: use "a not in b" instead of "not a in b" in the entire codebase +- Removed: clean_pyc command since it does not work correctly in many cases +- Removed: sync_media_s3 command in favor of sync_s3 +- BugFix: syncdata, use pk instead of id for identifying primary key of objects +- BugFix: sync_s3, use safer content type per default +- BugFix: export_emails, filtering on groups +- BugFix: print_user_for_session, use USERNAME_FIELD if defined +- BugFix: update_permission, fixed TypeError issue +- BugFix: JSONField, do not coerse a json string into a python list +- BugFix: import json issue by using absolute imports +- BugFix: add minimal version number to six (>=1.2) +- Docs: graph_models, Added some documentation about using dot templates +- Docs: reset_db, short description on SQL DDL used +- Docs: Added specific list of supported Python and Django versions +- Docs: Add link to GoDjango screencast +- Docs: Add ShortUUIDField to docs +- Python3: fixes to graph_models and export_emails for Python3 compatibility 1.3.3 ----- Changes: - - Docs: Made it clearer that Django Extensions requires Django 1.4 or higher - - Translations: FR Updated - - Python3: Fix for shell_plus +- Docs: Made it clearer that Django Extensions requires Django 1.4 or higher +- Translations: FR Updated +- Python3: Fix for shell_plus 1.3.0 ----- Changes: - - Feature: SQLDiff much better notnull detection - - Feature: reset_db add option to explicit set the PostGreSQL owner of the newly created DB - - Feature: shell_plus added support for MongoEngine - - Feature: sync_s3 enable syncing to other cloud providers compatible with s3 - - Improvement: ForeignKeyAutocompleteAdmin add option to limit queryset - - BugFix: graph_models fix issue with models without primary key - - BugFix: graph_models fix UnicodeDecodeError using --verbose-names - - BugFix: dumpscript fix problems with date/datetimes by saving them now as ISO8601 - - Docs: many improvements - - Docs: Chinese translation !!! - - Python3: various improvements - - Tests: add Django 1.6 + +- Feature: SQLDiff much better notnull detection +- Feature: reset_db add option to explicit set the PostGreSQL owner of the newly created DB +- Feature: shell_plus added support for MongoEngine +- Feature: sync_s3 enable syncing to other cloud providers compatible with s3 +- Improvement: ForeignKeyAutocompleteAdmin add option to limit queryset +- BugFix: graph_models fix issue with models without primary key +- BugFix: graph_models fix UnicodeDecodeError using --verbose-names +- BugFix: dumpscript fix problems with date/datetimes by saving them now as ISO8601 +- Docs: many improvements +- Docs: Chinese translation !!! +- Python3: various improvements +- Tests: add Django 1.6 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f488dbe81..7bf677598 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,7 @@ There are many ways to contribute to the project. You may improve the documentation, address a bug, add some feature to the code or do something else. All sort of contributions are welcome. - -### Development +## Development To start development on this project, fork this repository and follow the following instructions. @@ -22,11 +21,13 @@ $ source venv/bin/activate # for accessing the GUI portion of the test application (venv) $ export DJANGO_EXTENSIONS_DATABASE_NAME="db.sqlite3" # you may change if you want to use any other database +# run migrations +(venv) $ python manage.py migrate # start the development server (venv) $ python manage.py runserver ``` -### Testing +## Testing To run tests against a particular `python` and `django` version installed inside your virtual environment, you may use: diff --git a/Makefile b/Makefile index 246fcb032..b7bef3a96 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ clean-test: compile-catalog: for loc in django_extensions/locale/*; do \ - python setup.py compile_catalog --directory django_extensions/locale/ --locale $$(basename $$loc) --domain django || exit 1; \ + pybabel compile --directory django_extensions/locale/ --locale $$(basename $$loc) --domain django --statistics || exit 1; \ done test: @@ -42,4 +42,4 @@ coverage: test coverage html install: clean - python setup.py install + python -m pip install . diff --git a/README.rst b/README.rst index 37f8bc4d1..65687bffd 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ minutes Eric walks you through a half a dozen command extensions. There is also Requirements ============ -Django Extensions requires Django 2.2 or later. +Django Extensions requires Django 4.2 or later. Getting It @@ -57,18 +57,20 @@ You can get Django Extensions by using pip:: $ pip install django-extensions -If you want to install it from source, grab the git repository from GitHub and run setup.py:: +If you want to install it from source, grab the git repository from GitHub:: $ git clone git://github.com/django-extensions/django-extensions.git $ cd django-extensions - $ python setup.py install + $ pip install . Installing It ============= To enable `django_extensions` in your project you need to add it to `INSTALLED_APPS` in your projects -`settings.py` file:: +`settings.py` file: + +.. code-block:: python INSTALLED_APPS = ( ... @@ -108,8 +110,8 @@ Open Source projects can always use more help. Fixing a problem, documenting a f translation in your language. If you have some time to spare and like to help us, here are the places to do so: - 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/ +- Mailing list: https://groups.google.com/group/django-extensions +- Translations: https://www.transifex.com/projects/p/django-extensions/ Documentation @@ -132,6 +134,6 @@ Please remember that nobody is paid directly to develop or maintain Django Exten between putting food on the table, family, this project and the rest of life :-) -__ http://ericholscher.com/blog/2008/sep/12/screencast-django-command-extensions/ -__ http://vimeo.com/1720508 +__ https://ericholscher.com/blog/2008/sep/12/screencast-django-command-extensions/ +__ https://vimeo.com/1720508 __ https://www.youtube.com/watch?v=1F6G3ONhr4k diff --git a/django_extensions/__init__.py b/django_extensions/__init__.py index d4291672a..f93ccdf85 100644 --- a/django_extensions/__init__.py +++ b/django_extensions/__init__.py @@ -1,29 +1,5 @@ -# -*- coding: utf-8 -*- -VERSION = (3, 1, 4) - - -def get_version(version): - """Dynamically calculate the version based on VERSION tuple.""" - if len(version) > 2 and version[2] is not None: - if len(version) == 4: - str_version = "%s.%s.%s.%s" % version - elif isinstance(version[2], int): - str_version = "%s.%s.%s" % version[:3] - else: - str_version = "%s.%s_%s" % version[:3] - else: - str_version = "%s.%s" % version[:2] - - return str_version +from django.utils.version import get_version +VERSION = (4, 2, 0, "alpha", 0) __version__ = get_version(VERSION) - -try: - import django - - if django.VERSION < (3, 2): - 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/admin/__init__.py b/django_extensions/admin/__init__.py index 01bd709a3..cfb39ec0e 100644 --- a/django_extensions/admin/__init__.py +++ b/django_extensions/admin/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Autocomplete feature for admin panel # @@ -47,7 +46,7 @@ class ForeignKeyAutocompleteAdminMixin: related_search_fields = {} # type: Dict[str, Tuple[str]] related_string_functions = {} # type: Dict[str, Callable] - autocomplete_limit = getattr(settings, 'FOREIGNKEY_AUTOCOMPLETE_LIMIT', None) + autocomplete_limit = getattr(settings, "FOREIGNKEY_AUTOCOMPLETE_LIMIT", None) def get_urls(self): from django.urls import path @@ -55,11 +54,16 @@ def get_urls(self): def wrap(view): def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) + return update_wrapper(wrapper, view) return [ - path('foreignkey_autocomplete/', wrap(self.foreignkey_autocomplete), - name='%s_%s_autocomplete' % (self.model._meta.app_label, self.model._meta.model_name)) + path( + "foreignkey_autocomplete/", + wrap(self.foreignkey_autocomplete), + name="%s_%s_autocomplete" + % (self.model._meta.app_label, self.model._meta.model_name), + ) ] + super().get_urls() def foreignkey_autocomplete(self, request): @@ -67,11 +71,11 @@ def foreignkey_autocomplete(self, request): Search in the fields of the given related model and returns the result as a simple string to be used by the jQuery Autocomplete plugin """ - query = request.GET.get('q', None) - app_label = request.GET.get('app_label', None) - model_name = request.GET.get('model_name', None) - search_fields = request.GET.get('search_fields', None) - object_pk = request.GET.get('object_pk', None) + query = request.GET.get("q", None) + app_label = request.GET.get("app_label", None) + model_name = request.GET.get("model_name", None) + search_fields = request.GET.get("search_fields", None) + object_pk = request.GET.get("object_pk", None) try: to_string_function = self.related_string_functions[model_name] @@ -79,13 +83,14 @@ def foreignkey_autocomplete(self, request): to_string_function = lambda x: x.__str__() if search_fields and app_label and model_name and (query or object_pk): + def construct_search(field_name): # use different lookup methods depending on the notation - if field_name.startswith('^'): + if field_name.startswith("^"): return "%s__istartswith" % field_name[1:] - elif field_name.startswith('='): + elif field_name.startswith("="): return "%s__iexact" % field_name[1:] - elif field_name.startswith('@'): + elif field_name.startswith("@"): return "%s__search" % field_name[1:] else: return "%s__icontains" % field_name @@ -93,10 +98,15 @@ def construct_search(field_name): model = apps.get_model(app_label, model_name) queryset = model._default_manager.all() - data = '' + data = "" if query: for bit in query.split(): - or_queries = [models.Q(**{construct_search(smart_str(field_name)): smart_str(bit)}) for field_name in search_fields.split(',')] + or_queries = [ + models.Q( + **{construct_search(smart_str(field_name)): smart_str(bit)} + ) + for field_name in search_fields.split(",") + ] other_qs = QuerySet(model) other_qs.query.select_related = queryset.query.select_related other_qs = other_qs.filter(reduce(operator.or_, or_queries)) @@ -107,9 +117,11 @@ def construct_search(field_name): queryset = queryset.filter(additional_filter) if self.autocomplete_limit: - queryset = queryset[:self.autocomplete_limit] + queryset = queryset[: self.autocomplete_limit] - data = ''.join([str('%s|%s\n') % (to_string_function(f), f.pk) for f in queryset]) + data = "".join( + [str("%s|%s\n") % (to_string_function(f), f.pk) for f in queryset] + ) elif object_pk: try: obj = queryset.get(pk=object_pk) @@ -117,7 +129,7 @@ def construct_search(field_name): pass else: data = to_string_function(obj) - return HttpResponse(data, content_type='text/plain') + return HttpResponse(data, content_type="text/plain") return HttpResponseNotFound() def get_related_filter(self, model, request): @@ -133,33 +145,50 @@ def get_help_text(self, field_name, model_name): searchable_fields = self.related_search_fields.get(field_name, None) if searchable_fields: help_kwargs = { - 'model_name': model_name, - 'field_list': get_text_list(searchable_fields, _('and')), + "model_name": model_name, + "field_list": get_text_list(searchable_fields, _("and")), } - return _('Use the left field to do %(model_name)s lookups in the fields %(field_list)s.') % help_kwargs - return '' - - def formfield_for_dbfield(self, db_field, **kwargs): + return ( + _( + "Use the left field to do %(model_name)s lookups " + "in the fields %(field_list)s." + ) + % help_kwargs + ) + return "" + + def formfield_for_dbfield(self, db_field, request, **kwargs): """ Override the default widget for Foreignkey fields if they are specified in the related_search_fields class attribute. """ - if isinstance(db_field, models.ForeignKey) and db_field.name in self.related_search_fields: - help_text = self.get_help_text(db_field.name, db_field.remote_field.model._meta.object_name) - if kwargs.get('help_text'): - help_text = str('%s %s') % (kwargs['help_text'], help_text) - kwargs['widget'] = ForeignKeySearchInput(db_field.remote_field, self.related_search_fields[db_field.name]) - kwargs['help_text'] = help_text - return super().formfield_for_dbfield(db_field, **kwargs) + if ( + isinstance(db_field, models.ForeignKey) + and db_field.name in self.related_search_fields + ): + help_text = self.get_help_text( + db_field.name, db_field.remote_field.model._meta.object_name + ) + if kwargs.get("help_text"): + help_text = str("%s %s") % (kwargs["help_text"], help_text) + kwargs["widget"] = ForeignKeySearchInput( + db_field.remote_field, self.related_search_fields[db_field.name] + ) + kwargs["help_text"] = help_text + return super().formfield_for_dbfield(db_field, request, **kwargs) class ForeignKeyAutocompleteAdmin(ForeignKeyAutocompleteAdminMixin, admin.ModelAdmin): pass -class ForeignKeyAutocompleteTabularInline(ForeignKeyAutocompleteAdminMixin, admin.TabularInline): +class ForeignKeyAutocompleteTabularInline( + ForeignKeyAutocompleteAdminMixin, admin.TabularInline +): pass -class ForeignKeyAutocompleteStackedInline(ForeignKeyAutocompleteAdminMixin, admin.StackedInline): +class ForeignKeyAutocompleteStackedInline( + ForeignKeyAutocompleteAdminMixin, admin.StackedInline +): pass diff --git a/django_extensions/admin/filter.py b/django_extensions/admin/filter.py index cc9a67a7e..3a40def2a 100644 --- a/django_extensions/admin/filter.py +++ b/django_extensions/admin/filter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib.admin import FieldListFilter from django.contrib.admin.utils import prepare_lookup_value from django.utils.translation import gettext_lazy as _ @@ -6,7 +5,7 @@ class NullFieldListFilter(FieldListFilter): def __init__(self, field, request, params, model, model_admin, field_path): - self.lookup_kwarg = '{0}__isnull'.format(field_path) + self.lookup_kwarg = "{0}__isnull".format(field_path) super().__init__(field, request, params, model, model_admin, field_path) lookup_choices = self.lookups(request, model_admin) self.lookup_choices = () if lookup_choices is None else list(lookup_choices) @@ -19,23 +18,27 @@ def value(self): def lookups(self, request, model_admin): return ( - ('1', _('Yes')), - ('0', _('No')), + ("1", _("Yes")), + ("0", _("No")), ) def choices(self, cl): yield { - 'selected': self.value() is None, - 'query_string': cl.get_query_string({}, [self.lookup_kwarg]), - 'display': _('All'), + "selected": self.value() is None, + "query_string": cl.get_query_string({}, [self.lookup_kwarg]), + "display": _("All"), } for lookup, title in self.lookup_choices: yield { - 'selected': self.value() == prepare_lookup_value(self.lookup_kwarg, lookup), - 'query_string': cl.get_query_string({ - self.lookup_kwarg: lookup, - }, []), - 'display': title, + "selected": self.value() + == prepare_lookup_value(self.lookup_kwarg, lookup), + "query_string": cl.get_query_string( + { + self.lookup_kwarg: lookup, + }, + [], + ), + "display": title, } def queryset(self, request, queryset): @@ -48,6 +51,6 @@ def queryset(self, request, queryset): class NotNullFieldListFilter(NullFieldListFilter): def lookups(self, request, model_admin): return ( - ('0', _('Yes')), - ('1', _('No')), + ("0", _("Yes")), + ("1", _("No")), ) diff --git a/django_extensions/admin/widgets.py b/django_extensions/admin/widgets.py index 816fb4e62..69c12d2c5 100644 --- a/django_extensions/admin/widgets.py +++ b/django_extensions/admin/widgets.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import urllib from django import forms @@ -22,24 +21,24 @@ class ForeignKeySearchInput(ForeignKeyRawIdWidget): # Set this to the patch of the search view search_path = None - def _media(self): + @property + def media(self): js_files = [ - static('django_extensions/js/jquery.bgiframe.js'), - static('django_extensions/js/jquery.ajaxQueue.js'), - static('django_extensions/js/jquery.autocomplete.js'), + static("django_extensions/js/jquery.bgiframe.js"), + static("django_extensions/js/jquery.ajaxQueue.js"), + static("django_extensions/js/jquery.autocomplete.js"), ] return forms.Media( - css={'all': (static('django_extensions/css/jquery.autocomplete.css'), )}, + css={"all": (static("django_extensions/css/jquery.autocomplete.css"),)}, js=js_files, ) - media = property(_media) def label_for_value(self, value): key = self.rel.get_related_field().name obj = self.rel.model._default_manager.get(**{key: value}) - return Truncator(obj).words(14, truncate='...') + return Truncator(obj).words(14, truncate="...") def __init__(self, rel, search_fields, attrs=None): self.search_fields = search_fields @@ -51,40 +50,50 @@ def render(self, name, value, attrs=None, renderer=None): opts = self.rel.model._meta app_label = opts.app_label model_name = opts.object_name.lower() - related_url = reverse('admin:%s_%s_changelist' % (app_label, model_name)) + related_url = reverse("admin:%s_%s_changelist" % (app_label, model_name)) if not self.search_path: - self.search_path = urllib.parse.urljoin(related_url, 'foreignkey_autocomplete/') + self.search_path = urllib.parse.urljoin( + related_url, "foreignkey_autocomplete/" + ) params = self.url_parameters() if params: - url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) + url = "?" + "&".join(["%s=%s" % (k, v) for k, v in params.items()]) else: - url = '' + url = "" - if 'class' not in attrs: - attrs['class'] = 'vForeignKeyRawIdAdminField' + if "class" not in attrs: + attrs["class"] = "vForeignKeyRawIdAdminField" # Call the TextInput render method directly to have more control output = [forms.TextInput.render(self, name, value, attrs)] if value: label = self.label_for_value(value) else: - label = '' + label = "" context = { - 'url': url, - 'related_url': related_url, - 'search_path': self.search_path, - 'search_fields': ','.join(self.search_fields), - 'app_label': app_label, - 'model_name': model_name, - 'label': label, - 'name': name, + "url": url, + "related_url": related_url, + "search_path": self.search_path, + "search_fields": ",".join(self.search_fields), + "app_label": app_label, + "model_name": model_name, + "label": label, + "name": name, } - output.append(render_to_string(self.widget_template or ( - 'django_extensions/widgets/%s/%s/foreignkey_searchinput.html' % (app_label, model_name), - 'django_extensions/widgets/%s/foreignkey_searchinput.html' % app_label, - 'django_extensions/widgets/foreignkey_searchinput.html', - ), context)) + output.append( + render_to_string( + self.widget_template + or ( + "django_extensions/widgets/%s/%s/foreignkey_searchinput.html" + % (app_label, model_name), + "django_extensions/widgets/%s/foreignkey_searchinput.html" + % app_label, + "django_extensions/widgets/foreignkey_searchinput.html", + ), + context, + ) + ) output.reverse() - return mark_safe(''.join(output)) + return mark_safe("".join(output)) diff --git a/django_extensions/apps.py b/django_extensions/apps.py index 4c13d33f8..d01cbf0d0 100644 --- a/django_extensions/apps.py +++ b/django_extensions/apps.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- from django.apps import AppConfig class DjangoExtensionsConfig(AppConfig): - name = 'django_extensions' + name = "django_extensions" verbose_name = "Django Extensions" diff --git a/django_extensions/auth/mixins.py b/django_extensions/auth/mixins.py index 1fcf019d4..ef42d3427 100644 --- a/django_extensions/auth/mixins.py +++ b/django_extensions/auth/mixins.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- from django.contrib.auth.mixins import UserPassesTestMixin class ModelUserFieldPermissionMixin(UserPassesTestMixin): - model_permission_user_field = 'user' + model_permission_user_field = "user" def get_model_permission_user_field(self): return self.model_permission_user_field diff --git a/django_extensions/collision_resolvers.py b/django_extensions/collision_resolvers.py index e662ef30a..df28252bb 100644 --- a/django_extensions/collision_resolvers.py +++ b/django_extensions/collision_resolvers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import inspect import sys from abc import abstractmethod, ABCMeta @@ -19,7 +18,7 @@ class BaseCR(metaclass=ABCMeta): It receives Dict[str, List[str]], where key is model name and values are full model names (full model name means: module + model_name). You should return Dict[str, str], where key is model name and value is full model name. - """ + """ # noqa: E501 @classmethod def get_app_name_and_model(cls, full_model_path): # type: (str) -> Tuple[str, str] @@ -32,7 +31,11 @@ def resolve_collisions(self, namespace): # type: (Dict[str, List[str]]) -> Dict class LegacyCR(BaseCR): - """ Default collision resolver. Model from last application in alphabetical order is selected. """ + """ + Default collision resolver. + + Model from last application in alphabetical order is selected. + """ def resolve_collisions(self, namespace): result = {} @@ -45,7 +48,9 @@ class AppsOrderCR(LegacyCR, metaclass=ABCMeta): APP_PRIORITIES = None # type: List[str] def resolve_collisions(self, namespace): - assert self.APP_PRIORITIES is not None, "You must define APP_PRIORITIES in your resolver class!" + assert self.APP_PRIORITIES is not None, ( + "You must define APP_PRIORITIES in your resolver class!" + ) result = {} for name, models in namespace.items(): if len(models) > 0: @@ -71,12 +76,13 @@ class InstalledAppsOrderCR(AppsOrderCR): You can set your own app priorities list by subclassing him and overwriting APP_PRIORITIES field. This collision resolver will select model from first app on this list. If both app's are absent on this list, resolver will choose model from first app in alphabetical order. - """ + """ # noqa: E501 @property def APP_PRIORITIES(self): from django.conf import settings - return getattr(settings, 'INSTALLED_APPS', []) + + return getattr(settings, "INSTALLED_APPS", []) class PathBasedCR(LegacyCR, metaclass=ABCMeta): @@ -98,7 +104,9 @@ def resolve_collisions(self, namespace): continue for model in models: new_name = self.transform_import(model) - assert isinstance(new_name, str), "result of transform_import must be str!" + assert isinstance(new_name, str), ( + "result of transform_import must be str!" + ) base_imports[new_name] = model return base_imports @@ -108,12 +116,12 @@ class FullPathCR(PathBasedCR): Collision resolver which transform full model name to alias by changing dots to underscores. He also removes 'models' part of alias, because all models are in models.py files. Model from last application in alphabetical order is selected. - """ + """ # noqa: E501 def transform_import(self, module_path): - module, model = module_path.rsplit('.models', 1) + module, model = module_path.rsplit(".models", 1) module_path = module + model - return module_path.replace('.', '_') + return module_path.replace(".", "_") class AppNameCR(PathBasedCR, metaclass=ABCMeta): @@ -122,14 +130,16 @@ class AppNameCR(PathBasedCR, metaclass=ABCMeta): You must define MODIFICATION_STRING which should be string to format with two keyword arguments: app_name and model_name. For example: "{app_name}_{model_name}". Model from last application in alphabetical order is selected. - """ + """ # noqa: E501 MODIFICATION_STRING = None # type: Optional[str] def transform_import(self, module_path): - assert self.MODIFICATION_STRING is not None, "You must define MODIFICATION_STRING in your resolver class!" + assert self.MODIFICATION_STRING is not None, ( + "You must define MODIFICATION_STRING in your resolver class!" + ) app_name, model_name = self.get_app_name_and_model(module_path) - app_name = app_name.replace('.', '_') + app_name = app_name.replace(".", "_") return self.MODIFICATION_STRING.format(app_name=app_name, model_name=model_name) @@ -138,7 +148,7 @@ class AppNamePrefixCR(AppNameCR): Collision resolver which transform pair (app name, model_name) to alias "{app_name}_{model_name}". Model from last application in alphabetical order is selected. Result is different than FullPathCR, when model has app_label other than current app. - """ + """ # noqa: E501 MODIFICATION_STRING = "{app_name}_{model_name}" @@ -147,7 +157,7 @@ class AppNameSuffixCR(AppNameCR): """ Collision resolver which transform pair (app name, model_name) to alias "{model_name}_{app_name}" Model from last application in alphabetical order is selected. - """ + """ # noqa: E501 MODIFICATION_STRING = "{model_name}_{app_name}" @@ -156,7 +166,7 @@ class AppNamePrefixCustomOrderCR(AppNamePrefixCR, InstalledAppsOrderCR): """ Collision resolver which is mixin of AppNamePrefixCR and InstalledAppsOrderCR. In case of collisions he sets aliases like AppNamePrefixCR, but sets default model using InstalledAppsOrderCR. - """ + """ # noqa: E501 pass @@ -165,7 +175,7 @@ class AppNameSuffixCustomOrderCR(AppNameSuffixCR, InstalledAppsOrderCR): """ Collision resolver which is mixin of AppNameSuffixCR and InstalledAppsOrderCR. In case of collisions he sets aliases like AppNameSuffixCR, but sets default model using InstalledAppsOrderCR. - """ + """ # noqa: E501 pass @@ -174,7 +184,7 @@ class FullPathCustomOrderCR(FullPathCR, InstalledAppsOrderCR): """ Collision resolver which is mixin of FullPathCR and InstalledAppsOrderCR. In case of collisions he sets aliases like FullPathCR, but sets default model using InstalledAppsOrderCR. - """ + """ # noqa: E501 pass @@ -187,22 +197,26 @@ class AppLabelCR(PathBasedCR, metaclass=ABCMeta): This is different from AppNameCR when the app is nested with several level of namespace: Gives sites_Site instead of django_contrib_sites_Site Model from last application in alphabetical order is selected. - """ + """ # noqa: E501 MODIFICATION_STRING = None # type: Optional[str] def transform_import(self, module_path): - assert self.MODIFICATION_STRING is not None, "You must define MODIFICATION_STRING in your resolver class!" + assert self.MODIFICATION_STRING is not None, ( + "You must define MODIFICATION_STRING in your resolver class!" + ) model_class = import_string(module_path) app_label, model_name = model_class._meta.app_label, model_class.__name__ - return self.MODIFICATION_STRING.format(app_label=app_label, model_name=model_name) + return self.MODIFICATION_STRING.format( + app_label=app_label, model_name=model_name + ) class AppLabelPrefixCR(AppLabelCR): """ Collision resolver which transform pair (app_label, model_name) to alias "{app_label}_{model_name}". Model from last application in alphabetical order is selected. - """ + """ # noqa: E501 MODIFICATION_STRING = "{app_label}_{model_name}" @@ -211,7 +225,7 @@ class AppLabelSuffixCR(AppLabelCR): """ Collision resolver which transform pair (app_label, model_name) to alias "{model_name}_{app_label}". Model from last application in alphabetical order is selected. - """ + """ # noqa: E501 MODIFICATION_STRING = "{model_name}_{app_label}" @@ -228,10 +242,14 @@ def run_collision_resolver(self, models_to_import): @classmethod def _get_dictionary_of_names(cls, models_to_import): # type: (Dict[str, List[str]]) -> (Dict[str, str]) from django.conf import settings - collision_resolver_class = import_string(getattr( - settings, 'SHELL_PLUS_MODEL_IMPORTS_RESOLVER', - 'django_extensions.collision_resolvers.LegacyCR' - )) + + collision_resolver_class = import_string( + getattr( + settings, + "SHELL_PLUS_MODEL_IMPORTS_RESOLVER", + "django_extensions.collision_resolvers.LegacyCR", + ) + ) cls._assert_is_collision_resolver_class_correct(collision_resolver_class) result = collision_resolver_class().resolve_collisions(models_to_import) @@ -241,25 +259,35 @@ def _get_dictionary_of_names(cls, models_to_import): # type: (Dict[str, List[st @classmethod def _assert_is_collision_resolver_result_correct(cls, result): - assert isinstance(result, dict), "Result of resolve_collisions function must be a dict!" + assert isinstance(result, dict), ( + "Result of resolve_collisions function must be a dict!" + ) for key, value in result.items(): - assert isinstance(key, str), "key in collision resolver result should be str not %s" % key - assert isinstance(value, str), "value in collision resolver result should be str not %s" % value + assert isinstance(key, str), ( + "key in collision resolver result should be str not %s" % key + ) + assert isinstance(value, str), ( + "value in collision resolver result should be str not %s" % value + ) @classmethod def _assert_is_collision_resolver_class_correct(cls, collision_resolver_class): assert inspect.isclass(collision_resolver_class) and issubclass( - collision_resolver_class, BaseCR), "SHELL_PLUS_MODEL_IMPORTS_RESOLVER " \ - "must be subclass of BaseCR!" - assert len(inspect.getfullargspec(collision_resolver_class.resolve_collisions).args) == 2, \ - "resolve_collisions function must take one argument!" + collision_resolver_class, BaseCR + ), "SHELL_PLUS_MODEL_IMPORTS_RESOLVER must be subclass of BaseCR!" + assert ( + len( + inspect.getfullargspec(collision_resolver_class.resolve_collisions).args + ) + == 2 + ), "resolve_collisions function must take one argument!" @classmethod def _get_dictionary_of_modules(cls, dictionary_of_names): # type: (Dict[str, str]) -> Dict[str, List[Tuple[str, str]]] dictionary_of_modules = {} # type: Dict[str, List[Tuple[str, str]]] for alias, model in dictionary_of_names.items(): - module_path, model_name = model.rsplit('.', 1) + module_path, model_name = model.rsplit(".", 1) dictionary_of_modules.setdefault(module_path, []) dictionary_of_modules[module_path].append((model_name, alias)) return dictionary_of_modules diff --git a/django_extensions/compat.py b/django_extensions/compat.py index bb8ca1c9c..583286348 100644 --- a/django_extensions/compat.py +++ b/django_extensions/compat.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from io import BytesIO import csv @@ -19,6 +18,7 @@ def load_tag_library(libname): """ from django.template.backends.django import get_installed_libraries from django.template.library import InvalidTemplateLibrary + try: lib = get_installed_libraries()[libname] lib = importlib.import_module(lib).register @@ -28,8 +28,8 @@ def load_tag_library(libname): def get_template_setting(template_key, default=None): - """ Read template settings """ - templates_var = getattr(settings, 'TEMPLATES', None) + """Read template settings""" + templates_var = getattr(settings, "TEMPLATES", None) if templates_var: for tdict in templates_var: if template_key in tdict: diff --git a/django_extensions/db/fields/__init__.py b/django_extensions/db/fields/__init__.py index e320dfd42..20d4104da 100644 --- a/django_extensions/db/fields/__init__.py +++ b/django_extensions/db/fields/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Django Extensions additional model fields @@ -10,12 +9,14 @@ try: import uuid + HAS_UUID = True except ImportError: HAS_UUID = False try: import shortuuid + HAS_SHORT_UUID = True except ImportError: HAS_SHORT_UUID = False @@ -29,11 +30,12 @@ from django.utils.encoding import force_str -MAX_UNIQUE_QUERY_ATTEMPTS = getattr(settings, 'EXTENSIONS_MAX_UNIQUE_QUERY_ATTEMPTS', 100) +MAX_UNIQUE_QUERY_ATTEMPTS = getattr( + settings, "EXTENSIONS_MAX_UNIQUE_QUERY_ATTEMPTS", 100 +) class UniqueFieldMixin: - def check_is_bool(self, attrname): if not isinstance(getattr(self, attrname), bool): raise ValueError("'{}' argument must be True or False".format(attrname)) @@ -41,7 +43,8 @@ def check_is_bool(self, attrname): @staticmethod def _get_fields(model_cls): return [ - (f, f.model if f.model != model_cls else None) for f in model_cls._meta.get_fields() + (f, f.model if f.model != model_cls else None) + for f in model_cls._meta.get_fields() if not f.is_relation or f.one_to_one or (f.many_to_one and f.related_model) ] @@ -67,7 +70,7 @@ def find_unique(self, model_instance, field, iterator, *args): # for support django 2.2+ query = Q() - constraints = getattr(model_instance._meta, 'constraints', None) + constraints = getattr(model_instance._meta, "constraints", None) if constraints: unique_constraints = filter( lambda c: isinstance(c, UniqueConstraint), constraints @@ -143,14 +146,14 @@ def slugify_function(self, content): slug = AutoSlugField(populate_from='title') Inspired by SmileyChris' Unique Slugify snippet: - http://www.djangosnippets.org/snippets/690/ + https://www.djangosnippets.org/snippets/690/ """ def __init__(self, *args, **kwargs): - kwargs.setdefault('blank', True) - kwargs.setdefault('editable', False) + kwargs.setdefault("blank", True) + kwargs.setdefault("editable", False) - populate_from = kwargs.pop('populate_from', None) + populate_from = kwargs.pop("populate_from", None) if populate_from is None: raise ValueError("missing 'populate_from' argument") else: @@ -158,20 +161,25 @@ def __init__(self, *args, **kwargs): if not callable(populate_from): if not isinstance(populate_from, (list, tuple)): - populate_from = (populate_from, ) + populate_from = (populate_from,) if not all(isinstance(e, str) for e in populate_from): - raise TypeError("'populate_from' must be str or list[str] or tuple[str], found `%s`" % populate_from) - - self.slugify_function = kwargs.pop('slugify_function', slugify) - self.separator = kwargs.pop('separator', '-') - self.overwrite = kwargs.pop('overwrite', False) - self.check_is_bool('overwrite') - self.overwrite_on_add = kwargs.pop('overwrite_on_add', True) - self.check_is_bool('overwrite_on_add') - self.allow_duplicates = kwargs.pop('allow_duplicates', False) - self.check_is_bool('allow_duplicates') - self.max_unique_query_attempts = kwargs.pop('max_unique_query_attempts', MAX_UNIQUE_QUERY_ATTEMPTS) + raise TypeError( + "'populate_from' must be str or list[str] or tuple[str], found `%s`" + % populate_from + ) + + self.slugify_function = kwargs.pop("slugify_function", slugify) + self.separator = kwargs.pop("separator", "-") + self.overwrite = kwargs.pop("overwrite", False) + self.check_is_bool("overwrite") + self.overwrite_on_add = kwargs.pop("overwrite_on_add", True) + self.check_is_bool("overwrite_on_add") + self.allow_duplicates = kwargs.pop("allow_duplicates", False) + self.check_is_bool("allow_duplicates") + self.max_unique_query_attempts = kwargs.pop( + "max_unique_query_attempts", MAX_UNIQUE_QUERY_ATTEMPTS + ) super().__init__(*args, **kwargs) def _slug_strip(self, value): @@ -182,28 +190,31 @@ def _slug_strip(self, value): If an alternate separator is used, it will also replace any instances of the default '-' separator with the new separator. """ - re_sep = '(?:-|%s)' % re.escape(self.separator) - value = re.sub('%s+' % re_sep, self.separator, value) - return re.sub(r'^%s+|%s+$' % (re_sep, re_sep), '', value) + re_sep = "(?:-|%s)" % re.escape(self.separator) + value = re.sub("%s+" % re_sep, self.separator, value) + return re.sub(r"^%s+|%s+$" % (re_sep, re_sep), "", value) @staticmethod def slugify_func(content, slugify_function): if content: return slugify_function(content) - return '' + return "" def slug_generator(self, original_slug, start): yield original_slug for i in range(start, self.max_unique_query_attempts): slug = original_slug - end = '%s%s' % (self.separator, i) + end = "%s%s" % (self.separator, i) end_len = len(end) if self.slug_len and len(slug) + end_len > self.slug_len: - slug = slug[:self.slug_len - end_len] + slug = slug[: self.slug_len - end_len] slug = self._slug_strip(slug) - slug = '%s%s' % (slug, end) + slug = "%s%s" % (slug, end) yield slug - raise RuntimeError('max slug attempts for %s exceeded (%s)' % (original_slug, self.max_unique_query_attempts)) + raise RuntimeError( + "max slug attempts for %s exceeded (%s)" + % (original_slug, self.max_unique_query_attempts) + ) def create_slug(self, model_instance, add): slug = getattr(model_instance, self.attname) @@ -222,15 +233,17 @@ def create_slug(self, model_instance, add): # get fields to populate from and slug field to set populate_from = self._populate_from if not isinstance(populate_from, (list, tuple)): - populate_from = (populate_from, ) + populate_from = (populate_from,) slug_field = model_instance._meta.get_field(self.attname) - slugify_function = getattr(model_instance, 'slugify_function', self.slugify_function) + slugify_function = getattr( + model_instance, "slugify_function", self.slugify_function + ) # slugify the original field content and set next step to 2 slug_for_field = lambda lookup_value: self.slugify_func( self.get_slug_fields(model_instance, lookup_value), - slugify_function=slugify_function + slugify_function=slugify_function, ) slug = self.separator.join(map(slug_for_field, populate_from)) start = 2 @@ -239,7 +252,7 @@ def create_slug(self, model_instance, add): # and clean-up self.slug_len = slug_field.max_length if self.slug_len: - slug = slug[:self.slug_len] + slug = slug[: self.slug_len] slug = self._slug_strip(slug) original_slug = slug @@ -248,7 +261,8 @@ def create_slug(self, model_instance, add): return slug return self.find_unique( - model_instance, slug_field, self.slug_generator(original_slug, start)) + model_instance, slug_field, self.slug_generator(original_slug, start) + ) def get_slug_fields(self, model_instance, lookup_value): if callable(lookup_value): @@ -262,9 +276,10 @@ def get_slug_fields(self, model_instance, lookup_value): attr = getattr(attr, elem) except AttributeError: raise AttributeError( - "value {} in AutoSlugField's 'populate_from' argument {} returned an error - {} has no attribute {}".format( - elem, lookup_value, attr, elem)) - + "value {} in AutoSlugField's 'populate_from' argument {} returned an error - {} has no attribute {}".format( # noqa: E501 + elem, lookup_value, attr, elem + ) + ) if callable(attr): return "%s" % attr() @@ -279,13 +294,13 @@ def get_internal_type(self): def deconstruct(self): name, path, args, kwargs = super().deconstruct() - kwargs['populate_from'] = self._populate_from - if not self.separator == '-': - kwargs['separator'] = self.separator + kwargs["populate_from"] = self._populate_from + if not self.separator == "-": + kwargs["separator"] = self.separator if self.overwrite is not False: - kwargs['overwrite'] = True + kwargs["overwrite"] = True if self.allow_duplicates is not False: - kwargs['allow_duplicates'] = True + kwargs["allow_duplicates"] = True return name, path, args, kwargs @@ -319,41 +334,52 @@ class RandomCharField(UniqueFieldMixin, CharField): include_punctuation If set to True, include punctuation characters (default: False) + + keep_default + If set to True, keeps the default initialization value (default: False) """ def __init__(self, *args, **kwargs): - kwargs.setdefault('blank', True) - kwargs.setdefault('editable', False) + kwargs.setdefault("blank", True) + kwargs.setdefault("editable", False) - self.length = kwargs.pop('length', None) + self.length = kwargs.pop("length", None) if self.length is None: raise ValueError("missing 'length' argument") - kwargs['max_length'] = self.length + kwargs["max_length"] = self.length - self.lowercase = kwargs.pop('lowercase', False) - self.check_is_bool('lowercase') - self.uppercase = kwargs.pop('uppercase', False) - self.check_is_bool('uppercase') + self.lowercase = kwargs.pop("lowercase", False) + self.check_is_bool("lowercase") + self.uppercase = kwargs.pop("uppercase", False) + self.check_is_bool("uppercase") if self.uppercase and self.lowercase: - raise ValueError("the 'lowercase' and 'uppercase' arguments are mutually exclusive") - self.include_digits = kwargs.pop('include_digits', True) - self.check_is_bool('include_digits') - self.include_alpha = kwargs.pop('include_alpha', True) - self.check_is_bool('include_alpha') - self.include_punctuation = kwargs.pop('include_punctuation', False) - self.check_is_bool('include_punctuation') - self.max_unique_query_attempts = kwargs.pop('max_unique_query_attempts', MAX_UNIQUE_QUERY_ATTEMPTS) + raise ValueError( + "the 'lowercase' and 'uppercase' arguments are mutually exclusive" + ) + self.include_digits = kwargs.pop("include_digits", True) + self.check_is_bool("include_digits") + self.include_alpha = kwargs.pop("include_alpha", True) + self.check_is_bool("include_alpha") + self.include_punctuation = kwargs.pop("include_punctuation", False) + self.keep_default = kwargs.pop("keep_default", False) + self.check_is_bool("include_punctuation") + self.max_unique_query_attempts = kwargs.pop( + "max_unique_query_attempts", MAX_UNIQUE_QUERY_ATTEMPTS + ) # Set unique=False unless it's been set manually. - if 'unique' not in kwargs: - kwargs['unique'] = False + if "unique" not in kwargs: + kwargs["unique"] = False super().__init__(*args, **kwargs) def random_char_generator(self, chars): for i in range(self.max_unique_query_attempts): - yield ''.join(get_random_string(self.length, chars)) - raise RuntimeError('max random character attempts exceeded (%s)' % self.max_unique_query_attempts) + yield "".join(get_random_string(self.length, chars)) + raise RuntimeError( + "max random character attempts exceeded (%s)" + % self.max_unique_query_attempts + ) def in_unique_together(self, model_instance): for params in model_instance._meta.unique_together: @@ -362,10 +388,12 @@ def in_unique_together(self, model_instance): return False def pre_save(self, model_instance, add): - if not add and getattr(model_instance, self.attname) != '': + if (not add or self.keep_default) and getattr( + model_instance, self.attname + ) != "": return getattr(model_instance, self.attname) - population = '' + population = "" if self.include_alpha: if self.lowercase: population += string.ascii_lowercase @@ -397,20 +425,20 @@ def internal_type(self): def deconstruct(self): name, path, args, kwargs = super().deconstruct() - kwargs['length'] = self.length - del kwargs['max_length'] + kwargs["length"] = self.length + del kwargs["max_length"] if self.lowercase is True: - kwargs['lowercase'] = self.lowercase + kwargs["lowercase"] = self.lowercase if self.uppercase is True: - kwargs['uppercase'] = self.uppercase + kwargs["uppercase"] = self.uppercase if self.include_alpha is False: - kwargs['include_alpha'] = self.include_alpha + kwargs["include_alpha"] = self.include_alpha if self.include_digits is False: - kwargs['include_digits'] = self.include_digits + kwargs["include_digits"] = self.include_digits if self.include_punctuation is True: - kwargs['include_punctuation'] = self.include_punctuation + kwargs["include_punctuation"] = self.include_punctuation if self.unique is True: - kwargs['unique'] = self.unique + kwargs["unique"] = self.unique return name, path, args, kwargs @@ -422,9 +450,9 @@ class CreationDateTimeField(DateTimeField): """ def __init__(self, *args, **kwargs): - kwargs.setdefault('editable', False) - kwargs.setdefault('blank', True) - kwargs.setdefault('auto_now_add', True) + kwargs.setdefault("editable", False) + kwargs.setdefault("blank", True) + kwargs.setdefault("auto_now_add", True) DateTimeField.__init__(self, *args, **kwargs) def get_internal_type(self): @@ -433,11 +461,11 @@ def get_internal_type(self): def deconstruct(self): name, path, args, kwargs = super().deconstruct() if self.editable is not False: - kwargs['editable'] = True + kwargs["editable"] = True if self.blank is not True: - kwargs['blank'] = False + kwargs["blank"] = False if self.auto_now_add is not False: - kwargs['auto_now_add'] = True + kwargs["auto_now_add"] = True return name, path, args, kwargs @@ -451,7 +479,7 @@ class ModificationDateTimeField(CreationDateTimeField): """ def __init__(self, *args, **kwargs): - kwargs.setdefault('auto_now', True) + kwargs.setdefault("auto_now", True) DateTimeField.__init__(self, *args, **kwargs) def get_internal_type(self): @@ -460,11 +488,11 @@ def get_internal_type(self): def deconstruct(self): name, path, args, kwargs = super().deconstruct() if self.auto_now is not False: - kwargs['auto_now'] = True + kwargs["auto_now"] = True return name, path, args, kwargs def pre_save(self, model_instance, add): - if not getattr(model_instance, 'update_modified', True): + if not getattr(model_instance, "update_modified", True): return getattr(model_instance, self.attname) return super().pre_save(model_instance, add) @@ -480,23 +508,36 @@ class UUIDFieldMixin: By default uses UUID version 4 (randomly generated UUID). The field support all uuid versions which are natively supported by the uuid python module, except version 2. - For more information see: http://docs.python.org/lib/module-uuid.html - """ + For more information see: https://docs.python.org/lib/module-uuid.html + """ # noqa: E501 DEFAULT_MAX_LENGTH = 36 - def __init__(self, verbose_name=None, name=None, auto=True, version=4, - node=None, clock_seq=None, namespace=None, uuid_name=None, *args, - **kwargs): + def __init__( + self, + verbose_name=None, + name=None, + auto=True, + version=4, + node=None, + clock_seq=None, + namespace=None, + uuid_name=None, + *args, + **kwargs, + ): if not HAS_UUID: - raise ImproperlyConfigured("'uuid' module is required for UUIDField. (Do you have Python 2.5 or higher installed ?)") + raise ImproperlyConfigured( + "'uuid' module is required for UUIDField. " + "(Do you have Python 2.5 or higher installed ?)" + ) - kwargs.setdefault('max_length', self.DEFAULT_MAX_LENGTH) + kwargs.setdefault("max_length", self.DEFAULT_MAX_LENGTH) if auto: self.empty_strings_allowed = False - kwargs['blank'] = True - kwargs.setdefault('editable', False) + kwargs["blank"] = True + kwargs.setdefault("editable", False) self.auto = auto self.version = version @@ -535,28 +576,28 @@ def pre_save(self, model_instance, add): return value - def formfield(self, **kwargs): + def formfield(self, form_class=None, choices_form_class=None, **kwargs): if self.auto: return None - return super().formfield(**kwargs) + return super().formfield(form_class, choices_form_class, **kwargs) def deconstruct(self): name, path, args, kwargs = super().deconstruct() - if kwargs.get('max_length', None) == self.DEFAULT_MAX_LENGTH: - del kwargs['max_length'] + if kwargs.get("max_length", None) == self.DEFAULT_MAX_LENGTH: + del kwargs["max_length"] if self.auto is not True: - kwargs['auto'] = self.auto + kwargs["auto"] = self.auto if self.version != 4: - kwargs['version'] = self.version + kwargs["version"] = self.version if self.node is not None: - kwargs['node'] = self.node + kwargs["node"] = self.node if self.clock_seq is not None: - kwargs['clock_seq'] = self.clock_seq + kwargs["clock_seq"] = self.clock_seq if self.namespace is not None: - kwargs['namespace'] = self.namespace + kwargs["namespace"] = self.namespace if self.uuid_name is not None: - kwargs['uuid_name'] = self.name + kwargs["uuid_name"] = self.name return name, path, args, kwargs @@ -575,8 +616,11 @@ class ShortUUIDField(UUIDFieldMixin, CharField): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not HAS_SHORT_UUID: - raise ImproperlyConfigured("'shortuuid' module is required for ShortUUIDField. (Do you have Python 2.5 or higher installed ?)") - kwargs.setdefault('max_length', self.DEFAULT_MAX_LENGTH) + raise ImproperlyConfigured( + "'shortuuid' module is required for ShortUUIDField. " + "(Do you have Python 2.5 or higher installed ?)" + ) + kwargs.setdefault("max_length", self.DEFAULT_MAX_LENGTH) def create_uuid(self): if not self.version or self.version == 4: diff --git a/django_extensions/db/fields/json.py b/django_extensions/db/fields/json.py index 15dc74c67..b63c47e62 100644 --- a/django_extensions/db/fields/json.py +++ b/django_extensions/db/fields/json.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ JSONField automatically serializes most Python terms to JSON data. Creates a TEXT field with a default value of "{}". See test_json.py for @@ -10,10 +9,12 @@ class LOL(models.Model): extra = json.JSONField() """ + import json from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.db.models import expressions def dumps(value): @@ -51,7 +52,7 @@ class JSONField(models.TextField): """ def __init__(self, *args, **kwargs): - kwargs['default'] = kwargs.get('default', dict) + kwargs["default"] = kwargs.get("default", dict) models.TextField.__init__(self, *args, **kwargs) def get_default(self): @@ -66,7 +67,7 @@ def get_default(self): def to_python(self, value): """Convert our string value to JSON after we load it from the DB""" - if value is None or value == '': + if value is None or value == "": return {} if isinstance(value, str): @@ -93,15 +94,21 @@ def get_db_prep_save(self, value, connection, **kwargs): """Convert our JSON object to a string before we save""" if value is None and self.null: return None + # default values come in as strings; only non-strings should be # run through `dumps` - if not isinstance(value, str): + if ( + not isinstance(value, str) + # https://github.com/django-extensions/django-extensions/issues/1924 + # https://code.djangoproject.com/ticket/35167 + and not isinstance(value, expressions.Expression) + ): value = dumps(value) - return value + return super().get_db_prep_save(value, connection) def deconstruct(self): name, path, args, kwargs = super().deconstruct() - if self.default == '{}': - del kwargs['default'] + if self.default == "{}": + del kwargs["default"] return name, path, args, kwargs diff --git a/django_extensions/db/models.py b/django_extensions/db/models.py index d4c7d9b9c..cb7ea551b 100644 --- a/django_extensions/db/models.py +++ b/django_extensions/db/models.py @@ -1,9 +1,12 @@ -# -*- coding: utf-8 -*- from django.db import models from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from django_extensions.db.fields import AutoSlugField, CreationDateTimeField, ModificationDateTimeField +from django_extensions.db.fields import ( + AutoSlugField, + CreationDateTimeField, + ModificationDateTimeField, +) class TimeStampedModel(models.Model): @@ -14,15 +17,17 @@ class TimeStampedModel(models.Model): "modified" fields. """ - created = CreationDateTimeField(_('created')) - modified = ModificationDateTimeField(_('modified')) + created = CreationDateTimeField(_("created")) + modified = ModificationDateTimeField(_("modified")) def save(self, **kwargs): - self.update_modified = kwargs.pop('update_modified', getattr(self, 'update_modified', True)) + self.update_modified = kwargs.pop( + "update_modified", getattr(self, "update_modified", True) + ) super().save(**kwargs) class Meta: - get_latest_by = 'modified' + get_latest_by = "modified" abstract = True @@ -33,8 +38,8 @@ class TitleDescriptionModel(models.Model): An abstract base class model that provides title and description fields. """ - title = models.CharField(_('title'), max_length=255) - description = models.TextField(_('description'), blank=True, null=True) + title = models.CharField(_("title"), max_length=255) + description = models.TextField(_("description"), blank=True, null=True) class Meta: abstract = True @@ -55,7 +60,7 @@ class TitleSlugDescriptionModel(TitleDescriptionModel): See :py:class:`AutoSlugField` for more details. """ - slug = AutoSlugField(_('slug'), populate_from='title') + slug = AutoSlugField(_("slug"), populate_from="title") class Meta: abstract = True @@ -69,11 +74,11 @@ class ActivatorQuerySet(models.query.QuerySet): """ def active(self): - """ Return active query set """ + """Return active query set""" return self.filter(status=ActivatorModel.ACTIVE_STATUS) def inactive(self): - """ Return inactive query set """ + """Return inactive query set""" return self.filter(status=ActivatorModel.INACTIVE_STATUS) @@ -81,11 +86,12 @@ class ActivatorModelManager(models.Manager): """ ActivatorModelManager - Manager to return instances of ActivatorModel: SomeModel.objects.active() / .inactive() + Manager to return instances of ActivatorModel: + SomeModel.objects.active() / .inactive() """ def get_queryset(self): - """ Use ActivatorQuerySet for all results """ + """Use ActivatorQuerySet for all results""" return ActivatorQuerySet(model=self.model, using=self._db) def active(self): @@ -116,16 +122,25 @@ class ActivatorModel(models.Model): ACTIVE_STATUS = 1 STATUS_CHOICES = ( - (INACTIVE_STATUS, _('Inactive')), - (ACTIVE_STATUS, _('Active')), + (INACTIVE_STATUS, _("Inactive")), + (ACTIVE_STATUS, _("Active")), + ) + status = models.IntegerField( + _("status"), choices=STATUS_CHOICES, default=ACTIVE_STATUS + ) + activate_date = models.DateTimeField( + blank=True, null=True, help_text=_("keep empty for an immediate activation") + ) + deactivate_date = models.DateTimeField( + blank=True, null=True, help_text=_("keep empty for indefinite activation") ) - status = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=ACTIVE_STATUS) - activate_date = models.DateTimeField(blank=True, null=True, help_text=_('keep empty for an immediate activation')) - deactivate_date = models.DateTimeField(blank=True, null=True, help_text=_('keep empty for indefinite activation')) objects = ActivatorModelManager() class Meta: - ordering = ('status', '-activate_date',) + ordering = ( + "status", + "-activate_date", + ) abstract = True def save(self, *args, **kwargs): diff --git a/django_extensions/import_subclasses.py b/django_extensions/import_subclasses.py index 4e6140ca5..13d826c8b 100644 --- a/django_extensions/import_subclasses.py +++ b/django_extensions/import_subclasses.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from importlib import import_module from inspect import ( getmembers, @@ -39,19 +38,23 @@ def collect_subclasses(self): # type: () -> Dict[str, List[Tuple[str, str]]] but in future functionality of aliasing subclasses can be added. """ result = {} # type: Dict[str, List[Tuple[str, str]]] - for loader, module_name, is_pkg in walk_packages(path=[settings.BASE_DIR]): + for loader, module_name, is_pkg in walk_packages(path=[str(settings.BASE_DIR)]): subclasses_from_module = self._collect_classes_from_module(module_name) if subclasses_from_module: result[module_name] = subclasses_from_module return result def _collect_classes_from_module(self, module_name): # type: (str) -> List[Tuple[str, str]] - for excluded_module in getattr(settings, 'SHELL_PLUS_SUBCLASSES_IMPORT_MODULES_BLACKLIST', []): + for excluded_module in getattr( + settings, "SHELL_PLUS_SUBCLASSES_IMPORT_MODULES_BLACKLIST", [] + ): if module_name.startswith(excluded_module): return [] imported_module = import_module(module_name) classes_to_import = getmembers( - imported_module, lambda element: isclass(element) and element.__module__ == imported_module.__name__ + imported_module, + lambda element: isclass(element) + and element.__module__ == imported_module.__name__, ) classes_to_import = list(filter(self._should_be_imported, classes_to_import)) return [(name, name) for name, _ in classes_to_import] diff --git a/django_extensions/jobs/daily/cache_cleanup.py b/django_extensions/jobs/daily/cache_cleanup.py index cb92a822e..4eb1e4135 100644 --- a/django_extensions/jobs/daily/cache_cleanup.py +++ b/django_extensions/jobs/daily/cache_cleanup.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Daily cleanup job. @@ -16,9 +15,9 @@ class Job(DailyJob): help = "Cache (db) cleanup Job" def execute(self): - if hasattr(settings, 'CACHES'): + if hasattr(settings, "CACHES"): for cache_name, cache_options in settings.CACHES.items(): - if cache_options['BACKEND'].endswith("DatabaseCache"): + if cache_options["BACKEND"].endswith("DatabaseCache"): cache = caches[cache_name] cache.clear() return diff --git a/django_extensions/jobs/daily/daily_cleanup.py b/django_extensions/jobs/daily/daily_cleanup.py index ee08d3a11..e5e2b7176 100644 --- a/django_extensions/jobs/daily/daily_cleanup.py +++ b/django_extensions/jobs/daily/daily_cleanup.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Daily cleanup job. @@ -14,4 +13,5 @@ class Job(DailyJob): def execute(self): from django.core import management + management.call_command("clearsessions") diff --git a/django_extensions/locale/ar/LC_MESSAGES/django.po b/django_extensions/locale/ar/LC_MESSAGES/django.po new file mode 100644 index 000000000..89875dcb0 --- /dev/null +++ b/django_extensions/locale/ar/LC_MESSAGES/django.po @@ -0,0 +1,109 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-06 11:44+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: admin/__init__.py:139 +msgid "and" +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)s لبحث ضمن الأحقال التالية %(field_list)s " + +#: admin/filter.py:24 admin/filter.py:53 +msgid "Yes" +msgstr "نعم" + +#: admin/filter.py:25 admin/filter.py:54 +msgid "No" +msgstr "لا" + +#: admin/filter.py:32 +msgid "All" +msgstr "كل" + +#: db/models.py:18 +msgid "created" +msgstr "تم تكونه" + +#: db/models.py:19 +msgid "modified" +msgstr "تم تعديله" + +#: db/models.py:37 +msgid "title" +msgstr "عنوان" + +#: db/models.py:38 +msgid "description" +msgstr "وصف" + +#: db/models.py:59 +msgid "slug" +msgstr "رابط " + +#: db/models.py:120 mongodb/models.py:76 +msgid "Inactive" +msgstr "غير نشط" + +#: db/models.py:121 mongodb/models.py:77 +msgid "Active" +msgstr "نشط" + +#: db/models.py:123 +msgid "status" +msgstr "الحالة" + +#: db/models.py:124 mongodb/models.py:80 +msgid "keep empty for an immediate activation" +msgstr "أترك الحقل فارغ ليتم التنشيط مباشرة" + +#: db/models.py:125 mongodb/models.py:81 +msgid "keep empty for indefinite activation" +msgstr "أترك الحقل فارغ لتنشيط لمدة غير محددة" + +#: mongodb/fields/__init__.py:22 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "سلسلة الإحرف (طولها يصل إلى %(max_length)s)" + +#: validators.py:14 +msgid "Control Characters like new lines or tabs are not allowed." +msgstr "لا يسمح إستعمال أحرف تحكم مثل حرف العودة إلى السطر أو علامات التبويب" + +#: validators.py:48 +msgid "Leading and Trailing whitespaces are not allowed." +msgstr "المسافات البيضاء الزائدة عند البداية أو نهاية غير مسموح بها" + +#: validators.py:74 +msgid "Only a hex string is allowed." +msgstr "مسموح إستعمال سلسلة أحرف hex فقط" + +#: validators.py:75 +#, python-format +msgid "Invalid length. Must be %(length)d characters." +msgstr "الطول غير مقبول, يجب أن لا يكون أطول من %(length)d" + +#: validators.py:76 +#, python-format +msgid "Ensure that there are more than %(min)s characters." +msgstr "تأكد أن طول سلسلة الإحرف أطول من %(min)s " + +#: validators.py:77 +#, python-format +msgid "Ensure that there are no more than %(max)s characters." +msgstr "تأكد أن طول سلسلة الأحرف لا تتجوز %(max)s " diff --git a/django_extensions/locale/fr/LC_MESSAGES/django.po b/django_extensions/locale/fr/LC_MESSAGES/django.po index 0e9afbe19..7dce17f79 100644 --- a/django_extensions/locale/fr/LC_MESSAGES/django.po +++ b/django_extensions/locale/fr/LC_MESSAGES/django.po @@ -13,7 +13,7 @@ msgstr "" "POT-Creation-Date: 2011-02-02 11:42+0100\n" "PO-Revision-Date: 2014-01-11 11:14+0000\n" "Last-Translator: mathiasuk\n" -"Language-Team: French (http://www.transifex.com/projects/p/django-extensions/language/fr/)\n" +"Language-Team: French (https://www.transifex.com/projects/p/django-extensions/language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" diff --git a/django_extensions/logging/filters.py b/django_extensions/logging/filters.py index 7356d70c7..29995020a 100644 --- a/django_extensions/logging/filters.py +++ b/django_extensions/logging/filters.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import time import logging from hashlib import md5 @@ -12,8 +11,8 @@ def filter(self, record): from django.core.cache import cache # Rate is specified as 1 messages logged per N seconds. (aka cache timeout) - rate = getattr(settings, 'RATE_LIMITER_FILTER_RATE', 10) - prefix = getattr(settings, 'RATE_LIMITER_FILTER_PREFIX', 'ratelimiterfilter') + rate = getattr(settings, "RATE_LIMITER_FILTER_RATE", 10) + prefix = getattr(settings, "RATE_LIMITER_FILTER_PREFIX", "ratelimiterfilter") subject = record.getMessage() cache_key = "%s:%s" % (prefix, md5(subject).hexdigest()) diff --git a/django_extensions/management/base.py b/django_extensions/management/base.py index eac0dbc43..60ebf401e 100644 --- a/django_extensions/management/base.py +++ b/django_extensions/management/base.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- import sys from django.core.management.base import BaseCommand from logging import getLogger -logger = getLogger('django.commands') +logger = getLogger("django.commands") class LoggingBaseCommand(BaseCommand): @@ -49,5 +48,5 @@ def execute(self, *args, **options): try: super().execute(*args, **options) except Exception as e: - logger.error(e, exc_info=sys.exc_info(), extra={'status_code': 500}) + logger.error(e, exc_info=sys.exc_info(), extra={"status_code": 500}) raise diff --git a/django_extensions/management/color.py b/django_extensions/management/color.py index eb26e5ef4..884fa2b72 100644 --- a/django_extensions/management/color.py +++ b/django_extensions/management/color.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.core.management import color from django.utils import termcolors @@ -9,7 +8,7 @@ def _dummy_style_func(msg): def no_style(): style = color.no_style() - for role in ('INFO', 'WARN', 'BOLD', 'URL', 'MODULE', 'MODULE_NAME', 'URL_NAME'): + for role in ("INFO", "WARN", "BOLD", "URL", "MODULE", "MODULE_NAME", "URL_NAME"): setattr(style, role, _dummy_style_func) return style @@ -17,13 +16,13 @@ def no_style(): def color_style(): if color.supports_color(): style = color.color_style() - style.INFO = termcolors.make_style(fg='green') - style.WARN = termcolors.make_style(fg='yellow') - style.BOLD = termcolors.make_style(opts=('bold',)) - style.URL = termcolors.make_style(fg='green', opts=('bold',)) - style.MODULE = termcolors.make_style(fg='yellow') - style.MODULE_NAME = termcolors.make_style(opts=('bold',)) - style.URL_NAME = termcolors.make_style(fg='red') + style.INFO = termcolors.make_style(fg="green") + style.WARN = termcolors.make_style(fg="yellow") + style.BOLD = termcolors.make_style(opts=("bold",)) + style.URL = termcolors.make_style(fg="green", opts=("bold",)) + style.MODULE = termcolors.make_style(fg="yellow") + style.MODULE_NAME = termcolors.make_style(opts=("bold",)) + style.URL_NAME = termcolors.make_style(fg="red") else: style = no_style() return style diff --git a/django_extensions/management/commands/admin_generator.py b/django_extensions/management/commands/admin_generator.py index cf67870fc..d02b58c0d 100644 --- a/django_extensions/management/commands/admin_generator.py +++ b/django_extensions/management/commands/admin_generator.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The Django Admin Generator is a project which can automatically generate (scaffold) a Django Admin for you. By doing this it will introspect your @@ -25,47 +24,69 @@ from django_extensions.management.utils import signalcommand # Configurable constants -MAX_LINE_WIDTH = getattr(settings, 'MAX_LINE_WIDTH', 78) -INDENT_WIDTH = getattr(settings, 'INDENT_WIDTH', 4) -LIST_FILTER_THRESHOLD = getattr(settings, 'LIST_FILTER_THRESHOLD', 25) -RAW_ID_THRESHOLD = getattr(settings, 'RAW_ID_THRESHOLD', 100) - -LIST_FILTER = getattr(settings, 'LIST_FILTER', ( - models.DateField, - models.DateTimeField, - models.ForeignKey, - models.BooleanField, -)) - -SEARCH_FIELD_NAMES = getattr(settings, 'SEARCH_FIELD_NAMES', ( - 'name', - 'slug', -)) - -DATE_HIERARCHY_NAMES = getattr(settings, 'DATE_HIERARCHY_NAMES', ( - 'joined_at', - 'updated_at', - 'created_at', -)) - -PREPOPULATED_FIELD_NAMES = getattr(settings, 'PREPOPULATED_FIELD_NAMES', ( - 'slug=name', -)) - -PRINT_IMPORTS = getattr(settings, 'PRINT_IMPORTS', '''# -*- coding: utf-8 -*- +MAX_LINE_WIDTH = getattr(settings, "MAX_LINE_WIDTH", 78) +INDENT_WIDTH = getattr(settings, "INDENT_WIDTH", 4) +LIST_FILTER_THRESHOLD = getattr(settings, "LIST_FILTER_THRESHOLD", 25) +RAW_ID_THRESHOLD = getattr(settings, "RAW_ID_THRESHOLD", 100) + +LIST_FILTER = getattr( + settings, + "LIST_FILTER", + ( + models.DateField, + models.DateTimeField, + models.ForeignKey, + models.BooleanField, + ), +) + +SEARCH_FIELD_NAMES = getattr( + settings, + "SEARCH_FIELD_NAMES", + ( + "name", + "slug", + ), +) + +DATE_HIERARCHY_NAMES = getattr( + settings, + "DATE_HIERARCHY_NAMES", + ( + "joined_at", + "updated_at", + "created_at", + ), +) + +PREPOPULATED_FIELD_NAMES = getattr(settings, "PREPOPULATED_FIELD_NAMES", ("slug=name",)) + +PRINT_IMPORTS = getattr( + settings, + "PRINT_IMPORTS", + """# -*- coding: utf-8 -*- from django.contrib import admin from .models import %(models)s -''') +""", +) -PRINT_ADMIN_CLASS = getattr(settings, 'PRINT_ADMIN_CLASS', ''' +PRINT_ADMIN_CLASS = getattr( + settings, + "PRINT_ADMIN_CLASS", + """ @admin.register(%(name)s) class %(name)sAdmin(admin.ModelAdmin):%(class_)s -''') +""", +) -PRINT_ADMIN_PROPERTY = getattr(settings, 'PRINT_ADMIN_PROPERTY', ''' - %(key)s = %(value)s''') +PRINT_ADMIN_PROPERTY = getattr( + settings, + "PRINT_ADMIN_PROPERTY", + """ + %(key)s = %(value)s""", +) class UnicodeMixin: @@ -98,11 +119,11 @@ def __iter__(self): yield admin_model def __unicode__(self): - return ''.join(self._unicode_generator()) + return "".join(self._unicode_generator()) def _unicode_generator(self): models_list = [admin_model.name for admin_model in self] - yield PRINT_IMPORTS % dict(models=', '.join(models_list)) + yield PRINT_IMPORTS % dict(models=", ".join(models_list)) admin_model_names = [] for admin_model in self: @@ -113,7 +134,7 @@ def _unicode_generator(self): admin_model_names.append(admin_model.name) def __repr__(self): - return '<%s[%s]>' % ( + return "<%s[%s]>" % ( self.__class__.__name__, self.app.name, ) @@ -121,19 +142,24 @@ def __repr__(self): class AdminModel(UnicodeMixin): PRINTABLE_PROPERTIES = ( - 'list_display', - 'list_filter', - 'raw_id_fields', - 'search_fields', - 'prepopulated_fields', - 'date_hierarchy', + "list_display", + "list_filter", + "raw_id_fields", + "search_fields", + "prepopulated_fields", + "date_hierarchy", ) - def __init__(self, model, raw_id_threshold=RAW_ID_THRESHOLD, - list_filter_threshold=LIST_FILTER_THRESHOLD, - search_field_names=SEARCH_FIELD_NAMES, - date_hierarchy_names=DATE_HIERARCHY_NAMES, - prepopulated_field_names=PREPOPULATED_FIELD_NAMES, **options): + def __init__( + self, + model, + raw_id_threshold=RAW_ID_THRESHOLD, + list_filter_threshold=LIST_FILTER_THRESHOLD, + search_field_names=SEARCH_FIELD_NAMES, + date_hierarchy_names=DATE_HIERARCHY_NAMES, + prepopulated_field_names=PREPOPULATED_FIELD_NAMES, + **options, + ): self.model = model self.list_display = [] self.list_filter = [] @@ -148,7 +174,7 @@ def __init__(self, model, raw_id_threshold=RAW_ID_THRESHOLD, self.prepopulated_field_names = prepopulated_field_names def __repr__(self): - return '<%s[%s]>' % ( + return "<%s[%s]>" % ( self.__class__.__name__, self.name, ) @@ -160,8 +186,10 @@ def name(self): def _process_many_to_many(self, meta): raw_id_threshold = self.raw_id_threshold for field in meta.local_many_to_many: - if hasattr(field, 'remote_field'): - related_model = getattr(field.remote_field, 'related_model', field.remote_field.model) + if hasattr(field, "remote_field"): + related_model = getattr( + field.remote_field, "related_model", field.remote_field.model + ) else: raise CommandError("Unable to process ManyToMany relation") related_objects = related_model.objects.all() @@ -179,8 +207,10 @@ def _process_foreign_key(self, field): raw_id_threshold = self.raw_id_threshold list_filter_threshold = self.list_filter_threshold max_count = max(list_filter_threshold, raw_id_threshold) - if hasattr(field, 'remote_field'): - related_model = getattr(field.remote_field, 'related_model', field.remote_field.model) + if hasattr(field, "remote_field"): + related_model = getattr( + field.remote_field, "related_model", field.remote_field.model + ) else: raise CommandError("Unable to process ForeignKey relation") related_count = related_model.objects.all() @@ -213,7 +243,7 @@ def _process_field(self, field, parent_fields): return field_name def __unicode__(self): - return ''.join(self._unicode_generator()) + return "".join(self._unicode_generator()) def _yield_value(self, key, value): if isinstance(value, (list, set, tuple)): @@ -223,7 +253,7 @@ def _yield_value(self, key, value): elif isinstance(value, str): return self._yield_string(key, value) else: # pragma: no cover - raise TypeError('%s is not supported in %r' % (type(value), value)) + raise TypeError("%s is not supported in %r" % (type(value), value)) def _yield_string(self, key, value, converter=repr): return PRINT_ADMIN_PROPERTY % dict( @@ -235,12 +265,12 @@ def _yield_dict(self, key, value): row_parts = [] row = self._yield_string(key, value) if len(row) > MAX_LINE_WIDTH: - row_parts.append(self._yield_string(key, '{', str)) + row_parts.append(self._yield_string(key, "{", str)) for k, v in value.items(): - row_parts.append('%s%r: %r' % (2 * INDENT_WIDTH * ' ', k, v)) + row_parts.append("%s%r: %r" % (2 * INDENT_WIDTH * " ", k, v)) - row_parts.append(INDENT_WIDTH * ' ' + '}') - row = '\n'.join(row_parts) + row_parts.append(INDENT_WIDTH * " " + "}") + row = "\n".join(row_parts) return row @@ -248,12 +278,12 @@ def _yield_tuple(self, key, value): row_parts = [] row = self._yield_string(key, value) if len(row) > MAX_LINE_WIDTH: - row_parts.append(self._yield_string(key, '(', str)) + row_parts.append(self._yield_string(key, "(", str)) for v in value: - row_parts.append(2 * INDENT_WIDTH * ' ' + repr(v) + ',') + row_parts.append(2 * INDENT_WIDTH * " " + repr(v) + ",") - row_parts.append(INDENT_WIDTH * ' ' + ')') - row = '\n'.join(row_parts) + row_parts.append(INDENT_WIDTH * " " + ")") + row = "\n".join(row_parts) return row @@ -276,8 +306,8 @@ def _process(self): break for k in sorted(self.prepopulated_field_names): - k, vs = k.split('=', 1) - vs = vs.split(',') + k, vs = k.split("=", 1) + vs = vs.split(",") if k in field_names: incomplete = False for v in vs: @@ -292,56 +322,73 @@ def _process(self): class Command(LabelCommand): - help = '''Generate a `admin.py` file for the given app (models)''' + help = """Generate a `admin.py` file for the given app (models)""" # args = "[app_name]" can_import_settings = True def add_arguments(self, parser): - parser.add_argument('app_name') - parser.add_argument('model_name', nargs='*') + parser.add_argument("app_name") + parser.add_argument("model_name", nargs="*") parser.add_argument( - '-s', '--search-field', action='append', + "-s", + "--search-field", + action="append", default=SEARCH_FIELD_NAMES, - help='Fields named like this will be added to `search_fields`' - ' [default: %(default)s]') + help="Fields named like this will be added to `search_fields`" + " [default: %(default)s]", + ) parser.add_argument( - '-d', '--date-hierarchy', action='append', + "-d", + "--date-hierarchy", + action="append", default=DATE_HIERARCHY_NAMES, - help='A field named like this will be set as `date_hierarchy`' - ' [default: %(default)s]') + help="A field named like this will be set as `date_hierarchy`" + " [default: %(default)s]", + ) parser.add_argument( - '-p', '--prepopulated-fields', action='append', + "-p", + "--prepopulated-fields", + action="append", default=PREPOPULATED_FIELD_NAMES, - help='These fields will be prepopulated by the other field.' - 'The field names can be specified like `spam=eggA,eggB,eggC`' - ' [default: %(default)s]') + help="These fields will be prepopulated by the other field." + "The field names can be specified like `spam=eggA,eggB,eggC`" + " [default: %(default)s]", + ) parser.add_argument( - '-l', '--list-filter-threshold', type=int, - default=LIST_FILTER_THRESHOLD, metavar='LIST_FILTER_THRESHOLD', - help='If a foreign key has less than LIST_FILTER_THRESHOLD items ' - 'it will be added to `list_filter` [default: %(default)s]') + "-l", + "--list-filter-threshold", + type=int, + default=LIST_FILTER_THRESHOLD, + metavar="LIST_FILTER_THRESHOLD", + help="If a foreign key has less than LIST_FILTER_THRESHOLD items " + "it will be added to `list_filter` [default: %(default)s]", + ) parser.add_argument( - '-r', '--raw-id-threshold', type=int, - default=RAW_ID_THRESHOLD, metavar='RAW_ID_THRESHOLD', - help='If a foreign key has more than RAW_ID_THRESHOLD items ' - 'it will be added to `list_filter` [default: %(default)s]') + "-r", + "--raw-id-threshold", + type=int, + default=RAW_ID_THRESHOLD, + metavar="RAW_ID_THRESHOLD", + help="If a foreign key has more than RAW_ID_THRESHOLD items " + "it will be added to `list_filter` [default: %(default)s]", + ) @signalcommand def handle(self, *args, **options): - app_name = options['app_name'] + app_name = options["app_name"] try: app = apps.get_app_config(app_name) except LookupError: - self.stderr.write('This command requires an existing app name as argument') - self.stderr.write('Available apps:') + self.stderr.write("This command requires an existing app name as argument") + self.stderr.write("Available apps:") app_labels = [app.label for app in apps.get_app_configs()] for label in sorted(app_labels): - self.stderr.write(' %s' % label) + self.stderr.write(" %s" % label) return model_res = [] - for arg in options['model_name']: + for arg in options["model_name"]: model_res.append(re.compile(arg, re.IGNORECASE)) self.stdout.write(AdminApp(app, model_res, **options).__str__()) diff --git a/django_extensions/management/commands/clean_pyc.py b/django_extensions/management/commands/clean_pyc.py index f710eb511..5e1fd9a97 100644 --- a/django_extensions/management/commands/clean_pyc.py +++ b/django_extensions/management/commands/clean_pyc.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- 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,28 +12,37 @@ 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( - '--optimize', '-o', '-O', action='store_true', - dest='optimize', default=False, - help='Remove optimized python bytecode files' + "--optimize", + "-o", + "-O", + action="store_true", + dest="optimize", + default=False, + help="Remove optimized python bytecode files", ) parser.add_argument( - '--path', '-p', action='store', dest='path', - help='Specify path to recurse into' + "--path", + "-p", + action="store", + dest="path", + help="Specify path to recurse into", ) @signalcommand def handle(self, *args, **options): - project_root = options.get("path", getattr(settings, 'BASE_DIR', None)) + project_root = options.get("path", getattr(settings, "BASE_DIR", None)) if not project_root: - project_root = getattr(settings, 'BASE_DIR', None) + project_root = getattr(settings, "BASE_DIR", None) verbosity = options["verbosity"] if not project_root: - raise CommandError("No --path specified and settings.py does not contain BASE_DIR") + raise CommandError( + "No --path specified and settings.py does not contain BASE_DIR" + ) exts = options["optimize"] and "*.py[co]" or "*.pyc" diff --git a/django_extensions/management/commands/clear_cache.py b/django_extensions/management/commands/clear_cache.py index b022c61dc..17540af28 100644 --- a/django_extensions/management/commands/clear_cache.py +++ b/django_extensions/management/commands/clear_cache.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Author: AxiaCore S.A.S. http://axiacore.com +# Author: AxiaCore S.A.S. https://axiacore.com from django.conf import settings from django.core.cache import DEFAULT_CACHE_ALIAS, caches from django.core.cache.backends.base import InvalidCacheBackendError @@ -11,22 +10,27 @@ class Command(BaseCommand): """A simple management command which clears the site-wide cache.""" - help = 'Fully clear site-wide cache.' + help = "Fully clear site-wide cache." def add_arguments(self, parser): - parser.add_argument('--cache', action='append', - help='Name of cache to clear') - parser.add_argument('--all', '-a', action='store_true', default=False, - dest='all_caches', help='Clear all configured caches') + parser.add_argument("--cache", action="append", help="Name of cache to clear") + parser.add_argument( + "--all", + "-a", + action="store_true", + default=False, + dest="all_caches", + help="Clear all configured caches", + ) @signalcommand def handle(self, cache, all_caches, *args, **kwargs): if not cache and not all_caches: cache = [DEFAULT_CACHE_ALIAS] elif cache and all_caches: - raise CommandError('Using both --all and --cache is not supported') + raise CommandError("Using both --all and --cache is not supported") elif all_caches: - cache = getattr(settings, 'CACHES', {DEFAULT_CACHE_ALIAS: {}}).keys() + cache = getattr(settings, "CACHES", {DEFAULT_CACHE_ALIAS: {}}).keys() for key in cache: try: diff --git a/django_extensions/management/commands/compile_pyc.py b/django_extensions/management/commands/compile_pyc.py index 1b702fd7b..dc2d2d69d 100644 --- a/django_extensions/management/commands/compile_pyc.py +++ b/django_extensions/management/commands/compile_pyc.py @@ -1,8 +1,8 @@ -# -*- coding: utf-8 -*- import fnmatch 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,24 +12,31 @@ 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', - help='Specify path to recurse into') + parser.add_argument( + "--path", + "-p", + action="store", + dest="path", + help="Specify path to recurse into", + ) @signalcommand def handle(self, *args, **options): project_root = options["path"] if not project_root: - project_root = getattr(settings, 'BASE_DIR', None) + project_root = getattr(settings, "BASE_DIR", None) verbosity = options["verbosity"] if not project_root: - raise CommandError("No --path specified and settings.py does not contain BASE_DIR") + raise CommandError( + "No --path specified and settings.py does not contain BASE_DIR" + ) for root, dirs, filenames in os.walk(project_root): - for filename in fnmatch.filter(filenames, '*.py'): + for filename in fnmatch.filter(filenames, "*.py"): full_path = _j(root, filename) if verbosity > 1: self.stdout.write("Compiling %s...\n" % full_path) diff --git a/django_extensions/management/commands/create_command.py b/django_extensions/management/commands/create_command.py index 5844f91e3..23ecb6286 100644 --- a/django_extensions/management/commands/create_command.py +++ b/django_extensions/management/commands/create_command.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- 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 @@ -10,9 +10,10 @@ class Command(AppCommand): - help = "Creates a Django management command directory structure for the given app name in the app's directory." + 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 @@ -20,24 +21,33 @@ class Command(AppCommand): def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--name', '-n', action='store', dest='command_name', - default='sample', - help='The name to use for the management command' + "--name", + "-n", + action="store", + dest="command_name", + default="sample", + help="The name to use for the management command", ) parser.add_argument( - '--base', '-b', action='store', dest='base_command', - default='Base', help='The base class used for implementation of ' - 'this command. Should be one of Base, App, Label, or NoArgs' + "--base", + "-b", + action="store", + dest="base_command", + default="Base", + help="The base class used for implementation of " + "this command. Should be one of Base, App, Label, or NoArgs", ) parser.add_argument( - '--dry-run', action='store_true', default=False, - help='Do not actually create any files' + "--dry-run", + action="store_true", + default=False, + help="Do not actually create any files", ) @signalcommand def handle_app_config(self, args, **options): app = args - copy_template('command_template', app.path, **options) + copy_template("command_template", app.path, **options) def copy_template(template_name, copy_to, **options): @@ -45,29 +55,36 @@ def copy_template(template_name, copy_to, **options): import django_extensions style = color_style() - ERROR = getattr(style, 'ERROR', lambda x: x) - SUCCESS = getattr(style, 'SUCCESS', lambda x: x) + ERROR = getattr(style, "ERROR", lambda x: x) + SUCCESS = getattr(style, "SUCCESS", lambda x: x) - command_name, base_command = options['command_name'], '%sCommand' % options['base_command'] - dry_run = options['dry_run'] + command_name, base_command = ( + options["command_name"], + "%sCommand" % options["base_command"], + ) + dry_run = options["dry_run"] verbosity = options["verbosity"] - template_dir = os.path.join(django_extensions.__path__[0], 'conf', template_name) + template_dir = os.path.join(django_extensions.__path__[0], "conf", template_name) # walk the template structure and copies it for d, subdirs, files in os.walk(template_dir): - relative_dir = d[len(template_dir) + 1:] + relative_dir = d[len(template_dir) + 1 :] if relative_dir and not os.path.exists(os.path.join(copy_to, relative_dir)): if not dry_run: os.mkdir(os.path.join(copy_to, relative_dir)) for i, subdir in enumerate(subdirs): - if subdir.startswith('.'): + if subdir.startswith("."): del subdirs[i] for f in files: - if f.endswith(('.pyc', '.pyo')) or f.startswith(('.DS_Store', '__pycache__')): + if f.endswith((".pyc", ".pyo")) or f.startswith( + (".DS_Store", "__pycache__") + ): continue path_old = os.path.join(d, f) - path_new = os.path.join(copy_to, relative_dir, f.replace('sample', command_name)).rstrip(".tmpl") + path_new = os.path.join( + copy_to, relative_dir, f.replace("sample", command_name) + ).rstrip(".tmpl") if os.path.exists(path_new): path_new = os.path.join(copy_to, relative_dir, f).rstrip(".tmpl") if os.path.exists(path_new): @@ -76,16 +93,19 @@ def copy_template(template_name, copy_to, **options): continue if verbosity > 1: print(SUCCESS("%s" % path_new)) - with open(path_old, 'r') as fp_orig: + with open(path_old, "r") as fp_orig: data = fp_orig.read() - data = data.replace('{{ command_name }}', command_name) - data = data.replace('{{ base_command }}', base_command) + data = data.replace("{{ command_name }}", command_name) + data = data.replace("{{ base_command }}", base_command) if not dry_run: - with open(path_new, 'w') as fp_new: + with open(path_new, "w") as fp_new: fp_new.write(data) if not dry_run: try: shutil.copymode(path_old, path_new) _make_writeable(path_new) except OSError: - sys.stderr.write("Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" % path_new) + sys.stderr.write( + "Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" # noqa: E501 + % path_new + ) diff --git a/django_extensions/management/commands/create_jobs.py b/django_extensions/management/commands/create_jobs.py index 30c7e0cca..0fba5aaf8 100644 --- a/django_extensions/management/commands/create_jobs.py +++ b/django_extensions/management/commands/create_jobs.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- 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 @@ -10,16 +10,17 @@ class Command(AppCommand): - help = "Creates a Django jobs command directory structure for the given app name in the current directory." + 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 @signalcommand def handle_app_config(self, app, **options): - copy_template('jobs_template', app.path, **options) + copy_template("jobs_template", app.path, **options) def copy_template(template_name, copy_to, **options): @@ -27,22 +28,22 @@ def copy_template(template_name, copy_to, **options): import django_extensions style = color_style() - ERROR = getattr(style, 'ERROR', lambda x: x) - SUCCESS = getattr(style, 'SUCCESS', lambda x: x) + ERROR = getattr(style, "ERROR", lambda x: x) + SUCCESS = getattr(style, "SUCCESS", lambda x: x) - template_dir = os.path.join(django_extensions.__path__[0], 'conf', template_name) + template_dir = os.path.join(django_extensions.__path__[0], "conf", template_name) verbosity = options["verbosity"] # walks the template structure and copies it for d, subdirs, files in os.walk(template_dir): - relative_dir = d[len(template_dir) + 1:] + relative_dir = d[len(template_dir) + 1 :] if relative_dir and not os.path.exists(os.path.join(copy_to, relative_dir)): os.mkdir(os.path.join(copy_to, relative_dir)) for i, subdir in enumerate(subdirs): - if subdir.startswith('.'): + if subdir.startswith("."): del subdirs[i] for f in files: - if f.endswith('.pyc') or f.startswith('.DS_Store'): + if f.endswith(".pyc") or f.startswith(".DS_Store"): continue path_old = os.path.join(d, f) path_new = os.path.join(copy_to, relative_dir, f).rstrip(".tmpl") @@ -53,12 +54,15 @@ def copy_template(template_name, copy_to, **options): if verbosity > 1: print(SUCCESS("%s" % path_new)) - with open(path_old, 'r') as fp_orig: - with open(path_new, 'w') as fp_new: + with open(path_old, "r") as fp_orig: + with open(path_new, "w") as fp_new: fp_new.write(fp_orig.read()) try: shutil.copymode(path_old, path_new) _make_writeable(path_new) except OSError: - sys.stderr.write("Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" % path_new) + sys.stderr.write( + "Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" # noqa: E501 + % path_new + ) diff --git a/django_extensions/management/commands/create_template_tags.py b/django_extensions/management/commands/create_template_tags.py index c444e43f0..7666a0451 100644 --- a/django_extensions/management/commands/create_template_tags.py +++ b/django_extensions/management/commands/create_template_tags.py @@ -1,6 +1,6 @@ -# -*- coding: utf-8 -*- import os import sys +from typing import List from django.core.management.base import AppCommand @@ -8,20 +8,22 @@ class Command(AppCommand): - help = "Creates a Django template tags directory structure for the given app name in the apps's directory" + help = "Creates a Django template tags directory structure for the given app name " + "in the apps's directory" def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--name', - '-n', - action='store', - dest='tag_library_name', - default='appname_tags', - help='The name to use for the template tag base name. Defaults to `appname`_tags.' + "--name", + "-n", + action="store", + dest="tag_library_name", + default="appname_tags", + 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 @@ -29,10 +31,10 @@ def add_arguments(self, parser): @signalcommand def handle_app_config(self, app_config, **options): app_dir = app_config.path - tag_library_name = options['tag_library_name'] - if tag_library_name == 'appname_tags': - tag_library_name = '%s_tags' % os.path.basename(app_dir) - copy_template('template_tags_template', app_dir, tag_library_name) + tag_library_name = options["tag_library_name"] + if tag_library_name == "appname_tags": + tag_library_name = "%s_tags" % os.path.basename(app_dir) + copy_template("template_tags_template", app_dir, tag_library_name) def copy_template(template_name, copy_to, tag_library_name): @@ -40,28 +42,30 @@ def copy_template(template_name, copy_to, tag_library_name): import django_extensions import shutil - template_dir = os.path.join(django_extensions.__path__[0], 'conf', template_name) + template_dir = os.path.join(django_extensions.__path__[0], "conf", template_name) # walk the template structure and copies it for d, subdirs, files in os.walk(template_dir): - relative_dir = d[len(template_dir) + 1:] + relative_dir = d[len(template_dir) + 1 :] if relative_dir and not os.path.exists(os.path.join(copy_to, relative_dir)): os.mkdir(os.path.join(copy_to, relative_dir)) for i, subdir in enumerate(subdirs): - if subdir.startswith('.'): + if subdir.startswith("."): del subdirs[i] for f in files: - if f.endswith('.pyc') or f.startswith('.DS_Store'): + if f.endswith(".pyc") or f.startswith(".DS_Store"): continue path_old = os.path.join(d, f) - path_new = os.path.join(copy_to, relative_dir, f.replace('sample', tag_library_name)) + path_new = os.path.join( + copy_to, relative_dir, f.replace("sample", tag_library_name) + ) if os.path.exists(path_new): path_new = os.path.join(copy_to, relative_dir, f) if os.path.exists(path_new): continue path_new = path_new.rstrip(".tmpl") - fp_old = open(path_old, 'r') - fp_new = open(path_new, 'w') + fp_old = open(path_old, "r") + fp_new = open(path_new, "w") fp_new.write(fp_old.read()) fp_old.close() fp_new.close() @@ -69,4 +73,7 @@ def copy_template(template_name, copy_to, tag_library_name): shutil.copymode(path_old, path_new) _make_writeable(path_new) except OSError: - sys.stderr.write("Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" % path_new) + sys.stderr.write( + "Notice: Couldn't set permission bits on %s. You're probably using an uncommon filesystem setup. No problem.\n" # noqa: E501 + % path_new + ) diff --git a/django_extensions/management/commands/delete_squashed_migrations.py b/django_extensions/management/commands/delete_squashed_migrations.py index b6956a1c3..c41dcd7c8 100644 --- a/django_extensions/management/commands/delete_squashed_migrations.py +++ b/django_extensions/management/commands/delete_squashed_migrations.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +import difflib import os import inspect import re @@ -7,50 +7,67 @@ from django.db import DEFAULT_DB_ALIAS, connections from django.db.migrations.loader import AmbiguityError, MigrationLoader -REPLACES_REGEX = re.compile(r'\s+replaces\s*=\s*\[[^\]]+\]\s*') -PYC = '.pyc' +REPLACES_REGEX = re.compile(r"^\s+replaces\s*=\s*\[[^\]]+\]\s*?$", flags=re.MULTILINE) +PYC = ".pyc" def py_from_pyc(pyc_fn): - return pyc_fn[:-len(PYC)] + '.py' + return pyc_fn[: -len(PYC)] + ".py" class Command(BaseCommand): - help = "Deletes left over migrations that have been replaced by a " - "squashed migration and converts squashed migration into a normal " - "migration. Modifies your source tree! Use with care!" + help = ( + "Deletes left over migrations that have been replaced by a " + "squashed migration and converts squashed migration into a normal " + "migration. Modifies your source tree! Use with care!" + ) def add_arguments(self, parser): parser.add_argument( - 'app_label', - help='App label of the application to delete replaced migrations from.', + "app_label", + help="App label of the application to delete replaced migrations from.", ) parser.add_argument( - 'squashed_migration_name', default=None, nargs='?', - help='The squashed migration to replace. ' - 'If not specified defaults to the first found.' + "squashed_migration_name", + default=None, + nargs="?", + help="The squashed migration to replace. " + "If not specified defaults to the first found.", ) parser.add_argument( - '--noinput', '--no-input', action='store_false', dest='interactive', default=True, - help='Tells Django to NOT prompt the user for input of any kind.', + "--noinput", + "--no-input", + action="store_false", + dest="interactive", + default=True, + help="Tells Django to NOT prompt the user for input of any kind.", ) parser.add_argument( - '--dry-run', action='store_true', default=False, - help='Do not actually delete or change any files') + "--dry-run", + action="store_true", + default=False, + help="Do not actually delete or change any files", + ) parser.add_argument( - '--database', default=DEFAULT_DB_ALIAS, - help='Nominates a database to run command for. Defaults to the "%s" database.' % DEFAULT_DB_ALIAS, + "--database", + default=DEFAULT_DB_ALIAS, + help=( + "Nominates a database to run command for. " + 'Defaults to the "%s" database.' + ) + % DEFAULT_DB_ALIAS, ) def handle(self, **options): - self.verbosity = options['verbosity'] - self.interactive = options['interactive'] - self.dry_run = options['dry_run'] - app_label = options['app_label'] - squashed_migration_name = options['squashed_migration_name'] - database = options['database'] - - # Load the current graph state, check the app and migration they asked for exists + self.verbosity = options["verbosity"] + self.interactive = options["interactive"] + self.dry_run = options["dry_run"] + app_label = options["app_label"] + squashed_migration_name = options["squashed_migration_name"] + database = options["database"] + + # Load the current graph state + # check the app and migration they asked for exists loader = MigrationLoader(connections[database]) if app_label not in loader.migrated_apps: raise CommandError( @@ -60,18 +77,22 @@ def handle(self, **options): squashed_migration = None if squashed_migration_name: - squashed_migration = self.find_migration(loader, app_label, squashed_migration_name) + squashed_migration = self.find_migration( + loader, app_label, squashed_migration_name + ) if not squashed_migration.replaces: raise CommandError( - "The migration %s %s is not a squashed migration." % - (squashed_migration.app_label, squashed_migration.name) + "The migration %s %s is not a squashed migration." + % (squashed_migration.app_label, squashed_migration.name) ) else: leaf_nodes = loader.graph.leaf_nodes(app=app_label) migration = loader.get_migration(*leaf_nodes[0]) previous_migrations = [ loader.get_migration(al, mn) - for al, mn in loader.graph.forwards_plan((migration.app_label, migration.name)) + for al, mn in loader.graph.forwards_plan( + (migration.app_label, migration.name) + ) if al == migration.app_label ] migrations = previous_migrations + [migration] @@ -82,8 +103,7 @@ def handle(self, **options): if not squashed_migration: raise CommandError( - "Cannot find a squashed migration in app '%s'." % - (app_label) + "Cannot find a squashed migration in app '%s'." % (app_label) ) files_to_delete = [] @@ -92,8 +112,9 @@ def handle(self, **options): migration = loader.disk_migrations[al, mn] except KeyError: if self.verbosity > 0: - self.stderr.write("Couldn't find migration file for %s %s\n" - % (al, mn)) + self.stderr.write( + "Couldn't find migration file for %s %s\n" % (al, mn) + ) else: pyc_file = inspect.getfile(migration.__class__) files_to_delete.append(pyc_file) @@ -103,7 +124,9 @@ def handle(self, **options): # Tell them what we're doing and optionally ask if we should proceed if self.verbosity > 0 or self.interactive: - self.stdout.write(self.style.MIGRATE_HEADING("Will delete the following files:")) + self.stdout.write( + self.style.MIGRATE_HEADING("Will delete the following files:") + ) for fn in files_to_delete: self.stdout.write(" - %s" % fn) @@ -123,37 +146,46 @@ def handle(self, **options): if squashed_migration_fn.endswith(PYC): squashed_migration_fn = py_from_pyc(squashed_migration_fn) with open(squashed_migration_fn) as fp: - squashed_migration_lines = list(fp) - - delete_lines = [] - for i, line in enumerate(squashed_migration_lines): - if REPLACES_REGEX.match(line): - delete_lines.append(i) - if i > 0 and squashed_migration_lines[i - 1].strip() == '': - delete_lines.insert(0, i - 1) - break - if not delete_lines: + squashed_migration_content = fp.read() + + cleaned_migration_content = re.sub( + REPLACES_REGEX, "", squashed_migration_content + ) + if cleaned_migration_content == squashed_migration_content: raise CommandError( - ("Couldn't find 'replaces =' line in file %s. " - "Please finish cleaning up manually.") % (squashed_migration_fn,) + ( + "Couldn't find 'replaces =' lines in file %s. " + "Please finish cleaning up manually." + ) + % (squashed_migration_fn,) ) if self.verbosity > 0 or self.interactive: - self.stdout.write(self.style.MIGRATE_HEADING( - "Will delete line %s%s from file %s" % - (delete_lines[0], - ' and ' + str(delete_lines[1]) if len(delete_lines) > 1 else "", - squashed_migration_fn))) + # Print the differences between the original and new content + diff = difflib.unified_diff( + squashed_migration_content.splitlines(), + cleaned_migration_content.splitlines(), + lineterm="", + fromfile="Original", + tofile="Modified", + ) + + self.stdout.write( + self.style.MIGRATE_HEADING( + "The squashed migrations file %s will be modified like this :\n\n%s" + % ( + squashed_migration_fn, + "\n".join(diff), + ) + ) + ) if not self.confirm(): return - for line_num in sorted(delete_lines, reverse=True): - del squashed_migration_lines[line_num] - - with open(squashed_migration_fn, 'w') as fp: - if not self.dry_run: - fp.write("".join(squashed_migration_lines)) + if not self.dry_run: + with open(squashed_migration_fn, "w") as fp: + fp.write(cleaned_migration_content) def confirm(self): if self.interactive: @@ -178,6 +210,6 @@ def find_migration(self, loader, app_label, name): ) except KeyError: raise CommandError( - "Cannot find a migration matching '%s' from app '%s'." % - (name, app_label) + "Cannot find a migration matching '%s' from app '%s'." + % (name, app_label) ) diff --git a/django_extensions/management/commands/describe_form.py b/django_extensions/management/commands/describe_form.py index 609002cc9..78f4b24da 100644 --- a/django_extensions/management/commands/describe_form.py +++ b/django_extensions/management/commands/describe_form.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.apps import apps from django.core.management.base import CommandError, LabelCommand from django.utils.encoding import force_str @@ -10,25 +9,28 @@ class Command(LabelCommand): help = "Outputs the specified model as a form definition to the shell." def add_arguments(self, parser): - parser.add_argument('label', type=str, - help='application name and model name') + parser.add_argument("label", type=str, help="application name and model name") parser.add_argument( - "--fields", "-f", action="append", dest="fields", default=[], - help="Describe form with these fields only" + "--fields", + "-f", + action="append", + dest="fields", + default=[], + help="Describe form with these fields only", ) @signalcommand def handle(self, *args, **options): - label = options['label'] - fields = options['fields'] + label = options["label"] + fields = options["fields"] return describe_form(label, fields) def describe_form(label, fields): - """ Return a string describing a form based on the model """ + """Return a string describing a form based on the model""" try: - app_name, model_name = label.split('.')[-2:] + app_name, model_name = label.split(".")[-2:] except (IndexError, ValueError): raise CommandError("Need application and model name in the form: appname.model") model = apps.get_model(app_name, model_name) @@ -41,34 +43,53 @@ def describe_form(label, fields): if fields and f.name not in fields: continue formfield = f.formfield() - if '__dict__' not in dir(formfield): + if "__dict__" not in dir(formfield): continue attrs = {} - valid_fields = ['required', 'initial', 'max_length', 'min_length', 'max_value', 'min_value', 'max_digits', 'decimal_places', 'choices', 'help_text', 'label'] + valid_fields = [ + "required", + "initial", + "max_length", + "min_length", + "max_value", + "min_value", + "max_digits", + "decimal_places", + "choices", + "help_text", + "label", + ] for k, v in formfield.__dict__.items(): if k in valid_fields and v is not None: # ignore defaults, to minimize verbosity - if k == 'required' and v: + if k == "required" and v: continue - if k == 'help_text' and not v: + if k == "help_text" and not v: continue - if k == 'widget': + if k == "widget": attrs[k] = v.__class__ - elif k in ['help_text', 'label']: + elif k in ["help_text", "label"]: attrs[k] = str(force_str(v).strip()) else: attrs[k] = v - params = ', '.join(['%s=%r' % (k, v) for k, v in sorted(attrs.items())]) - field_list.append(' %(field_name)s = forms.%(field_type)s(%(params)s)' % { - 'field_name': f.name, - 'field_type': formfield.__class__.__name__, - 'params': params - }) - return ''' + params = ", ".join(["%s=%r" % (k, v) for k, v in sorted(attrs.items())]) + field_list.append( + " %(field_name)s = forms.%(field_type)s(%(params)s)" + % { + "field_name": f.name, + "field_type": formfield.__class__.__name__, + "params": params, + } + ) + return """ from django import forms from %(app_name)s.models import %(object_name)s class %(object_name)sForm(forms.Form): %(field_list)s -''' % {'app_name': app_name, 'object_name': opts.object_name, 'field_list': '\n'.join(field_list)} +""" % { + "app_name": app_name, + "object_name": opts.object_name, + "field_list": "\n".join(field_list), + } diff --git a/django_extensions/management/commands/drop_test_database.py b/django_extensions/management/commands/drop_test_database.py index be2089dc5..106ebf4c7 100644 --- a/django_extensions/management/commands/drop_test_database.py +++ b/django_extensions/management/commands/drop_test_database.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +import importlib.util from itertools import count import os import logging @@ -21,68 +21,104 @@ class Command(BaseCommand): def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--noinput', action='store_false', dest='interactive', - default=True, help='Tells Django to NOT prompt the user for input of any kind.' + "--noinput", + "--no-input", + action="store_false", + dest="interactive", + default=True, + help="Tells Django to NOT prompt the user for input of any kind.", ) parser.add_argument( - '-U', '--user', action='store', dest='user', default=None, - help='Use another user for the database then defined in settings.py' + "-U", + "--user", + action="store", + dest="user", + default=None, + help="Use another user for the database then defined in settings.py", ) parser.add_argument( - '-P', '--password', action='store', dest='password', default=None, - help='Use another password for the database then defined in settings.py' + "-P", + "--password", + action="store", + dest="password", + default=None, + help="Use another password for the database then defined in settings.py", ) parser.add_argument( - '-D', '--dbname', action='store', dest='dbname', default=None, - help='Use another database name then defined in settings.py' + "-D", + "--dbname", + action="store", + dest="dbname", + default=None, + help="Use another database name then defined in settings.py", ) parser.add_argument( - '-R', '--router', action='store', dest='router', default=DEFAULT_DB_ALIAS, - help='Use this router-database other then defined in settings.py' + "-R", + "--router", + action="store", + dest="router", + default=DEFAULT_DB_ALIAS, + help="Use this router-database other then defined in settings.py", ) parser.add_argument( - '--database', default=DEFAULT_DB_ALIAS, - help='Nominates a database to run command for. Defaults to the "%s" database.' % DEFAULT_DB_ALIAS, + "--database", + default=DEFAULT_DB_ALIAS, + help=( + "Nominates a database to run command for. " + 'Defaults to the "%s" database.' + ) + % DEFAULT_DB_ALIAS, ) @signalcommand def handle(self, *args, **options): """Drop test database for this project.""" - database = options['database'] - if options['router'] != DEFAULT_DB_ALIAS: - warnings.warn("--router is deprecated. You should use --database.", RemovedInNextVersionWarning, stacklevel=2) - database = options['router'] + database = options["database"] + if options["router"] != DEFAULT_DB_ALIAS: + warnings.warn( + "--router is deprecated. You should use --database.", + RemovedInNextVersionWarning, + stacklevel=2, + ) + database = options["router"] dbinfo = settings.DATABASES.get(database) if dbinfo is None: raise CommandError("Unknown database %s" % database) - engine = dbinfo.get('ENGINE') + engine = dbinfo.get("ENGINE") - user = password = database_name = database_host = database_port = '' - if engine == 'mysql': - (user, password, database_name, database_host, database_port) = parse_mysql_cnf(dbinfo) + user = password = database_name = database_host = database_port = "" + if engine == "mysql": + (user, password, database_name, database_host, database_port) = ( + parse_mysql_cnf(dbinfo) + ) - user = options['user'] or dbinfo.get('USER') or user - password = options['password'] or dbinfo.get('PASSWORD') or password + user = options["user"] or dbinfo.get("USER") or user + password = options["password"] or dbinfo.get("PASSWORD") or password try: - database_name = dbinfo['TEST']['NAME'] + database_name = dbinfo["TEST"]["NAME"] except KeyError: database_name = None if database_name is None: - database_name = TEST_DATABASE_PREFIX + (options['dbname'] or dbinfo.get('NAME')) + database_name = TEST_DATABASE_PREFIX + ( + options["dbname"] or dbinfo.get("NAME") + ) - if database_name is None or database_name == '': - raise CommandError("You need to specify DATABASE_NAME in your Django settings file.") + if database_name is None or database_name == "": + raise CommandError( + "You need to specify DATABASE_NAME in your Django settings file." + ) - database_host = dbinfo.get('HOST') or database_host - database_port = dbinfo.get('PORT') or database_port + database_host = dbinfo.get("HOST") or database_host + database_port = dbinfo.get("PORT") or database_port verbosity = options["verbosity"] - if options['interactive']: - confirm = input(""" + if options["interactive"]: + confirm = input( + """ You have requested to drop all test databases. This will IRREVERSIBLY DESTROY ALL data in the database "{db_name}" @@ -91,11 +127,12 @@ def handle(self, *args, **options): named "{db_name}_1", "{db_name}_2", etc.). Are you sure you want to do this? -Type 'yes' to continue, or 'no' to cancel: """.format(db_name=database_name)) +Type 'yes' to continue, or 'no' to cancel: """.format(db_name=database_name) + ) else: - confirm = 'yes' + confirm = "yes" - if confirm != 'yes': + if confirm != "yes": print("Reset cancelled.") return @@ -127,7 +164,7 @@ def format_filename(name, number): # replicated here. If fixed in Django, this code should be # updated accordingly. # Reference: https://code.djangoproject.com/ticket/32582 - return '{}_{}.{}'.format(filename, number, ext) + return "{}_{}.{}".format(filename, number, ext) try: for db_name in get_database_names(format_filename): @@ -140,59 +177,70 @@ def format_filename(name, number): elif engine in MYSQL_ENGINES: import MySQLdb as Database + kwargs = { - 'user': user, - 'passwd': password, + "user": user, + "passwd": password, } - if database_host.startswith('/'): - kwargs['unix_socket'] = database_host + if database_host.startswith("/"): + kwargs["unix_socket"] = database_host else: - kwargs['host'] = database_host + kwargs["host"] = database_host if database_port: - kwargs['port'] = int(database_port) + kwargs["port"] = int(database_port) connection = Database.connect(**kwargs) cursor = connection.cursor() - for db_name in get_database_names('{}_{}'.format): - exists_query = \ - "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='%s';" \ - % db_name + for db_name in get_database_names("{}_{}".format): + exists_query = ( + "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA " + "WHERE SCHEMA_NAME='%s';" % db_name + ) row_count = cursor.execute(exists_query) if row_count < 1: break - drop_query = 'DROP DATABASE IF EXISTS `%s`' % db_name + drop_query = "DROP DATABASE IF EXISTS `%s`" % db_name logging.info('Executing: "' + drop_query + '"') cursor.execute(drop_query) elif engine in POSTGRESQL_ENGINES: - import psycopg2 as Database # NOQA + has_psycopg3 = importlib.util.find_spec("psycopg") + if has_psycopg3: + import psycopg as Database # NOQA + else: + import psycopg2 as Database # NOQA - conn_params = {'database': 'template1'} + conn_params = {"dbname": "template1"} if user: - conn_params['user'] = user + conn_params["user"] = user if password: - conn_params['password'] = password + conn_params["password"] = password if database_host: - conn_params['host'] = database_host + conn_params["host"] = database_host if database_port: - conn_params['port'] = database_port + conn_params["port"] = database_port connection = Database.connect(**conn_params) - connection.set_isolation_level(0) # autocommit false + if has_psycopg3: + connection.autocommit = True + else: + connection.set_isolation_level(0) # autocommit false cursor = connection.cursor() - for db_name in get_database_names('{}_{}'.format): - exists_query = "SELECT datname FROM pg_catalog.pg_database WHERE datname='%s';" \ + for db_name in get_database_names("{}_{}".format): + exists_query = ( + "SELECT datname FROM pg_catalog.pg_database WHERE datname='%s';" % db_name + ) try: cursor.execute(exists_query) - # NOTE: Unlike MySQLdb, the psycopg2 cursor does not return the row count - # however both cursors provide it as a property + # NOTE: Unlike MySQLdb, the psycopg2 cursor does not return the row + # count however both cursors provide it as a property if cursor.rowcount < 1: break - drop_query = "DROP DATABASE IF EXISTS \"%s\";" % db_name + drop_query = 'DROP DATABASE IF EXISTS "%s";' % db_name logging.info('Executing: "' + drop_query + '"') cursor.execute(drop_query) except Database.ProgrammingError as e: @@ -201,5 +249,5 @@ def format_filename(name, number): else: raise CommandError("Unknown database engine %s" % engine) - if verbosity >= 2 or options['interactive']: + if verbosity >= 2 or options["interactive"]: print("Reset successful.") diff --git a/django_extensions/management/commands/dumpscript.py b/django_extensions/management/commands/dumpscript.py index 086a8c81e..d7d1f7bf2 100644 --- a/django_extensions/management/commands/dumpscript.py +++ b/django_extensions/management/commands/dumpscript.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- """ Title: Dumpscript management command Project: Hardytools (queryset-refactor version) - Author: Will Hardy (http://willhardy.com.au) + Author: Will Hardy Date: June 2008 Usage: python manage.py dumpscript appname > scripts/scriptname.py $Revision: 217 $ @@ -28,6 +27,7 @@ See TODOs and FIXMEs scattered throughout :-) """ + import datetime import sys @@ -37,11 +37,16 @@ from django.core.management.base import BaseCommand from django.db import router from django.db.models import ( - AutoField, BooleanField, DateField, DateTimeField, FileField, ForeignKey, + AutoField, + BooleanField, + DateField, + DateTimeField, + FileField, + ForeignKey, ) from django.db.models.deletion import Collector from django.utils import timezone -from django.utils.encoding import force_str, smart_text +from django.utils.encoding import force_str, smart_str from django_extensions.management.utils import signalcommand @@ -59,7 +64,11 @@ def orm_item_locator(orm_obj): original_pk_name = pk_name pk_value = getattr(orm_obj, pk_name) - while hasattr(pk_value, "_meta") and hasattr(pk_value._meta, "pk") and hasattr(pk_value._meta.pk, "name"): + while ( + hasattr(pk_value, "_meta") + and hasattr(pk_value._meta, "pk") + and hasattr(pk_value._meta.pk, "name") + ): the_class = pk_value._meta.object_name pk_name = pk_value._meta.pk.name pk_value = getattr(pk_value, pk_name) @@ -70,33 +79,43 @@ 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) - clean_dict[key] = StrToCodeChanger('dateutil.parser.parse("%s")' % v.isoformat()) + 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) output = """ importer.locate_object(%s, "%s", %s, "%s", %s, %s ) """ % ( - original_class, original_pk_name, - the_class, pk_name, pk_value, clean_dict + original_class, + original_pk_name, + the_class, + pk_name, + pk_value, + clean_dict, ) return output class Command(BaseCommand): - help = 'Dumps the data as a customised python script.' + help = "Dumps the data as a customised python script." def add_arguments(self, parser): super().add_arguments(parser) - parser.add_argument('appname', nargs='+') + parser.add_argument("appname", nargs="+") parser.add_argument( - '--autofield', action='store_false', dest='skip_autofield', - default=True, help='Include Autofields (like pk fields)' + "--autofield", + action="store_false", + dest="skip_autofield", + default=True, + help="Include Autofields (like pk fields)", ) @signalcommand def handle(self, *args, **options): - app_labels = options['appname'] + app_labels = options["appname"] # Get the models we want to export models = get_models(app_labels) @@ -125,17 +144,21 @@ def get_models(app_labels): Or at least discovered with a get_or_create() call. """ - # These models are not to be output, e.g. because they can be generated automatically + # These models are not to be outputted, + # e.g. because they can be generated automatically # TODO: This should be "appname.modelname" string - EXCLUDED_MODELS = (ContentType, ) + EXCLUDED_MODELS = (ContentType,) models = [] # If no app labels are given, return all if not app_labels: for app in apps.get_app_configs(): - models += [m for m in apps.get_app_config(app.label).get_models() - if m not in EXCLUDED_MODELS] + models += [ + m + for m in apps.get_app_config(app.label).get_models() + if m not in EXCLUDED_MODELS + ] return models # Get all relevant apps @@ -146,8 +169,11 @@ def get_models(app_labels): models.append(apps.get_model(app_label, model_name)) # Get all models for a given app else: - models += [m for m in apps.get_app_config(app_label).get_models() - if m not in EXCLUDED_MODELS] + models += [ + m + for m in apps.get_app_config(app_label).get_models() + if m not in EXCLUDED_MODELS + ] return models @@ -161,7 +187,6 @@ class Code: """ def __init__(self, indent=-1, stdout=None, stderr=None): - if not stdout: stdout = sys.stdout if not stderr: @@ -172,24 +197,30 @@ def __init__(self, indent=-1, stdout=None, stderr=None): self.stderr = stderr def __str__(self): - """ Return a string representation of this script. """ + """Return a string representation of this script.""" if self.imports: self.stderr.write(repr(self.import_lines)) - return flatten_blocks([""] + self.import_lines + [""] + self.lines, num_indents=self.indent) + return flatten_blocks( + [""] + self.import_lines + [""] + self.lines, num_indents=self.indent + ) else: return flatten_blocks(self.lines, num_indents=self.indent) def get_import_lines(self): - """ Take the stored imports and converts them to lines """ + """Take the stored imports and converts them to lines""" if self.imports: - return ["from %s import %s" % (value, key) for key, value in self.imports.items()] + return [ + "from %s import %s" % (value, key) + for key, value in self.imports.items() + ] else: return [] + import_lines = property(get_import_lines) class ModelCode(Code): - """ Produces a python script that can recreate data for a given model class. """ + """Produces a python script that can recreate data for a given model class.""" def __init__(self, model, context=None, stdout=None, stderr=None, options=None): super().__init__(indent=0, stdout=stdout, stderr=stderr) @@ -205,7 +236,8 @@ def get_imports(self): Return a dictionary of import statements, with the variable being defined as the key. """ - return {self.model.__name__: smart_text(self.model.__module__)} + return {self.model.__name__: smart_str(self.model.__module__)} + imports = property(get_imports) def get_lines(self): @@ -216,7 +248,14 @@ def get_lines(self): code = [] for counter, item in enumerate(self.model._default_manager.all()): - instance = InstanceCode(instance=item, id=counter + 1, context=self.context, stdout=self.stdout, stderr=self.stderr, options=self.options) + instance = InstanceCode( + instance=item, + id=counter + 1, + context=self.context, + stdout=self.stdout, + stderr=self.stderr, + options=self.options, + ) self.instances.append(instance) if instance.waiting_list: code += instance.lines @@ -233,10 +272,12 @@ def get_lines(self): class InstanceCode(Code): - """ Produces a python script that can recreate data for a given model instance. """ + """Produces a python script that can recreate data for a given model instance.""" - def __init__(self, instance, id, context=None, stdout=None, stderr=None, options=None): - """ We need the instance in question and an id """ + def __init__( + self, instance, id, context=None, stdout=None, stderr=None, options=None + ): + """We need the instance in question and an id""" super().__init__(indent=0, stdout=stdout, stderr=stderr) self.imports = {} @@ -260,7 +301,9 @@ def __init__(self, instance, id, context=None, stdout=None, stderr=None, options continue except AttributeError: pass - self.many_to_many_waiting_list[field] = list(getattr(self.instance, field.name).all()) + self.many_to_many_waiting_list[field] = list( + getattr(self.instance, field.name).all() + ) def get_lines(self, force=False): """ @@ -293,11 +336,15 @@ def get_lines(self, force=False): # Print the save command for our new object # e.g. model_name_35.save() if code_lines: - code_lines.append("%s = importer.save_or_locate(%s)\n" % (self.variable_name, self.variable_name)) + code_lines.append( + "%s = importer.save_or_locate(%s)\n" + % (self.variable_name, self.variable_name) + ) code_lines += self.get_many_to_many_lines(force=force) return code_lines + lines = property(get_lines) def skip(self): @@ -322,7 +369,7 @@ def skip(self): # since this instance isn't explicitly created, it's variable name # can't be referenced in the script, so record None in context dict pk_name = self.instance._meta.pk.name - key = '%s_%s' % (self.model.__name__, getattr(self.instance, pk_name)) + key = "%s_%s" % (self.model.__name__, getattr(self.instance, pk_name)) self.context[key] = None self.skip_me = True else: @@ -331,7 +378,7 @@ def skip(self): return self.skip_me def instantiate(self): - """ Write lines for instantiation """ + """Write lines for instantiation""" # e.g. model_name_35 = Model() code_lines = [] @@ -341,23 +388,31 @@ def instantiate(self): # Store our variable name for future foreign key references pk_name = self.instance._meta.pk.name - key = '%s_%s' % (self.model.__name__, getattr(self.instance, pk_name)) + key = "%s_%s" % (self.model.__name__, getattr(self.instance, pk_name)) self.context[key] = self.variable_name return code_lines def get_waiting_list(self, force=False): - """ Add lines for any waiting fields that can be completed now. """ + """Add lines for any waiting fields that can be completed now.""" code_lines = [] - skip_autofield = self.options['skip_autofield'] + skip_autofield = self.options["skip_autofield"] # Process normal fields for field in list(self.waiting_list): try: # Find the value, add the line, remove from waiting list and move on - value = get_attribute_value(self.instance, field, self.context, force=force, skip_autofield=skip_autofield) - code_lines.append('%s.%s = %s' % (self.variable_name, field.name, value)) + value = get_attribute_value( + self.instance, + field, + self.context, + force=force, + skip_autofield=skip_autofield, + ) + code_lines.append( + "%s.%s = %s" % (self.variable_name, field.name, value) + ) self.waiting_list.remove(field) except SkipValue: # Remove from the waiting list and move on @@ -370,7 +425,7 @@ def get_waiting_list(self, force=False): return code_lines def get_many_to_many_lines(self, force=False): - """ Generate lines that define many to many relations for this instance. """ + """Generate lines that define many to many relations for this instance.""" lines = [] @@ -378,15 +433,25 @@ def get_many_to_many_lines(self, force=False): for rel_item in list(rel_items): try: pk_name = rel_item._meta.pk.name - key = '%s_%s' % (rel_item.__class__.__name__, getattr(rel_item, pk_name)) + key = "%s_%s" % ( + rel_item.__class__.__name__, + getattr(rel_item, pk_name), + ) value = "%s" % self.context[key] - lines.append('%s.%s.add(%s)' % (self.variable_name, field.name, value)) + lines.append( + "%s.%s.add(%s)" % (self.variable_name, field.name, value) + ) self.many_to_many_waiting_list[field].remove(rel_item) except KeyError: if force: item_locator = orm_item_locator(rel_item) - self.context["__extra_imports"][rel_item._meta.object_name] = rel_item.__module__ - lines.append('%s.%s.add( %s )' % (self.variable_name, field.name, item_locator)) + self.context["__extra_imports"][rel_item._meta.object_name] = ( + rel_item.__module__ + ) + lines.append( + "%s.%s.add( %s )" + % (self.variable_name, field.name, item_locator) + ) self.many_to_many_waiting_list[field].remove(rel_item) if lines: @@ -396,7 +461,7 @@ def get_many_to_many_lines(self, force=False): class Script(Code): - """ Produces a complete python script that can recreate data for the given apps. """ + """Produces a complete python script that can recreate data for the given apps.""" def __init__(self, models, context=None, stdout=None, stderr=None, options=None): super().__init__(stdout=stdout, stderr=stderr) @@ -431,7 +496,13 @@ def _queue_models(self, models, context): # If the model is ready to be processed, add it to the list if check_dependencies(model, model_queue, context["__avaliable_models"]): - model_class = ModelCode(model=model, context=context, stdout=self.stdout, stderr=self.stderr, options=self.options) + model_class = ModelCode( + model=model, + context=context, + stdout=self.stdout, + stderr=self.stderr, + options=self.options, + ) model_queue.append(model_class) # Otherwise put the model back at the end of the list @@ -445,8 +516,17 @@ def _queue_models(self, models, context): if number_remaining_models == previous_number_remaining_models: allowed_cycles -= 1 if allowed_cycles <= 0: - # Add the remaining models, but do not remove them from the model list - missing_models = [ModelCode(model=m, context=context, stdout=self.stdout, stderr=self.stderr, options=self.options) for m in models] + # Add remaining models, but do not remove them from the model list + missing_models = [ + ModelCode( + model=m, + context=context, + stdout=self.stdout, + stderr=self.stderr, + options=self.options, + ) + for m in models + ] model_queue += missing_models # Replace the models with the model class objects # (sure, this is a little bit of hackery) @@ -466,7 +546,10 @@ def get_lines(self): # Queue and process the required models for model_class in self._queue_models(self.models, context=self.context): - msg = 'Processing model: %s.%s\n' % (model_class.model.__module__, model_class.model.__name__) + msg = "Processing model: %s.%s\n" % ( + model_class.model.__module__, + model_class.model.__name__, + ) self.stderr.write(msg) code.append(" # " + msg) code.append(model_class.import_lines) @@ -475,7 +558,10 @@ def get_lines(self): # Process left over foreign keys from cyclic models for model in self.models: - msg = 'Re-processing model: %s.%s\n' % (model.model.__module__, model.model.__name__) + msg = "Re-processing model: %s.%s\n" % ( + model.model.__module__, + model.model.__name__, + ) self.stderr.write(msg) code.append(" # " + msg) for instance in model.instances: @@ -501,7 +587,7 @@ def get_lines(self): # Instead of changing it, create a file called import_helper.py # and put there a class called ImportHelper(object) in it. # -# This class will be specially casted so that instead of extending object, +# This class will be specially cast so that instead of extending object, # it will actually extend the class BasicImportHelper() # # That means you just have to overload the methods you want to @@ -624,12 +710,13 @@ def run(): def import_data(): -""" % " ".join(sys.argv) +""" % " ".join(sys.argv) # noqa: E501 # HELPER FUNCTIONS # ------------------------------------------------------------------------------- + def flatten_blocks(lines, num_indents=-1): """ Take a list (block) or string (statement) and flattens it into a string @@ -650,18 +737,22 @@ def flatten_blocks(lines, num_indents=-1): def get_attribute_value(item, field, context, force=False, skip_autofield=True): - """ Get a string version of the given attribute's value, like repr() might. """ + """Get a string version of the given attribute's value, like repr() might.""" # Find the value of the field, catching any database issues try: value = getattr(item, field.name) except ObjectDoesNotExist: - raise SkipValue('Could not find object for %s.%s, ignoring.\n' % (item.__class__.__name__, field.name)) + raise SkipValue( + "Could not find object for %s.%s, ignoring.\n" + % (item.__class__.__name__, field.name) + ) # AutoField: We don't include the auto fields, they'll be automatically recreated if skip_autofield and isinstance(field, AutoField): raise SkipValue() - # Some databases (eg MySQL) might store boolean values as 0/1, this needs to be cast as a bool + # Some databases (eg MySQL) might store boolean values as 0/1, + # this needs to be cast as a bool elif isinstance(field, BooleanField) and value is not None: return repr(bool(value)) @@ -671,17 +762,19 @@ def get_attribute_value(item, field, context, force=False, skip_autofield=True): # ForeignKey fields, link directly using our stored python variable name elif isinstance(field, ForeignKey) and value is not None: - # Special case for contenttype foreign keys: no need to output any # content types in this script, as they can be generated again # automatically. # NB: Not sure if "is" will always work if field.remote_field.model is ContentType: - return 'ContentType.objects.get(app_label="%s", model="%s")' % (value.app_label, value.model) + return 'ContentType.objects.get(app_label="%s", model="%s")' % ( + value.app_label, + value.model, + ) # Generate an identifier (key) for this foreign object pk_name = value._meta.pk.name - key = '%s_%s' % (value.__class__.__name__, getattr(value, pk_name)) + key = "%s_%s" % (value.__class__.__name__, getattr(value, pk_name)) if key in context: variable_name = context[key] @@ -696,10 +789,10 @@ def get_attribute_value(item, field, context, force=False, skip_autofield=True): item_locator = orm_item_locator(value) return item_locator else: - raise DoLater('(FK) %s.%s\n' % (item.__class__.__name__, field.name)) + raise DoLater("(FK) %s.%s\n" % (item.__class__.__name__, field.name)) elif isinstance(field, (DateField, DateTimeField)) and value is not None: - return "dateutil.parser.parse(\"%s\")" % value.isoformat() + return 'dateutil.parser.parse("%s")' % value.isoformat() # A normal field (e.g. a python built-in) else: @@ -715,9 +808,12 @@ def make_clean_dict(the_dict): def check_dependencies(model, model_queue, avaliable_models): - """ Check that all the depenedencies for this model are already in the queue. """ + """Check that all the depenedencies for this model are already in the queue.""" # A list of allowed links: existing fields, itself and the special case ContentType - allowed_links = [m.model.__name__ for m in model_queue] + [model.__name__, 'ContentType'] + allowed_links = [m.model.__name__ for m in model_queue] + [ + model.__name__, + "ContentType", + ] # For each ForeignKey or ManyToMany field, check that a link is possible @@ -741,16 +837,16 @@ def check_dependencies(model, model_queue, avaliable_models): # EXCEPTIONS # ------------------------------------------------------------------------------- + class SkipValue(Exception): - """ Value could not be parsed or should simply be skipped. """ + """Value could not be parsed or should simply be skipped.""" class DoLater(Exception): - """ Value could not be parsed or should simply be skipped. """ + """Value could not be parsed or should simply be skipped.""" class StrToCodeChanger: - def __init__(self, string): self.repr = string diff --git a/django_extensions/management/commands/export_emails.py b/django_extensions/management/commands/export_emails.py index 39c61446a..142292f9c 100644 --- a/django_extensions/management/commands/export_emails.py +++ b/django_extensions/management/commands/export_emails.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import sys import csv @@ -11,29 +10,29 @@ FORMATS = [ - 'address', - 'emails', - 'google', - 'outlook', - 'linkedin', - 'vcard', + "address", + "emails", + "google", + "outlook", + "linkedin", + "vcard", ] def full_name(**kwargs): """Return full name or username.""" - first_name = kwargs.get('first_name') - last_name = kwargs.get('last_name') + first_name = kwargs.get("first_name") + last_name = kwargs.get("last_name") name = " ".join(n for n in [first_name, last_name] if n) if name: return name - name = kwargs.get('name') + name = kwargs.get("name") if name: return name - username = kwargs.get('username') + username = kwargs.get("username") if username: return username @@ -43,10 +42,10 @@ def full_name(**kwargs): class Command(BaseCommand): help = "Export user email address list in one of a number of formats." args = "[output file]" - label = 'filename to save to' + label = "filename to save to" can_import_settings = True - encoding = 'utf-8' # RED_FLAG: add as an option -DougN + encoding = "utf-8" # RED_FLAG: add as an option -DougN def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -54,45 +53,70 @@ def __init__(self, *args, **kwargs): def add_arguments(self, parser): super().add_arguments(parser) + ( + parser.add_argument( + "--group", + "-g", + action="store", + dest="group", + default=None, + help="Limit to users which are part of the supplied group name", + ), + ) parser.add_argument( - '--group', '-g', action='store', dest='group', default=None, - help='Limit to users which are part of the supplied group name', - ), - parser.add_argument( - '--format', '-f', action='store', dest='format', default=FORMATS[0], + "--format", + "-f", + action="store", + dest="format", + default=FORMATS[0], help="output format. May be one of %s." % ", ".join(FORMATS), ) def full_name(self, **kwargs): - return getattr(settings, 'EXPORT_EMAILS_FULL_NAME_FUNC', full_name)(**kwargs) + return getattr(settings, "EXPORT_EMAILS_FULL_NAME_FUNC", full_name)(**kwargs) @signalcommand def handle(self, *args, **options): if len(args) > 1: raise CommandError("extra arguments supplied") - group = options['group'] + group = options["group"] if group and not Group.objects.filter(name=group).count() == 1: - names = "', '".join(g['name'] for g in Group.objects.values('name')) + names = "', '".join(g["name"] for g in Group.objects.values("name")) if names: names = "'" + names + "'." - raise CommandError("Unknown group '" + group + "'. Valid group names are: " + names) + raise CommandError( + "Unknown group '" + group + "'. Valid group names are: " + names + ) UserModel = get_user_model() - order_by = getattr(settings, 'EXPORT_EMAILS_ORDER_BY', ['last_name', 'first_name', 'username', 'email']) - fields = getattr(settings, 'EXPORT_EMAILS_FIELDS', ['last_name', 'first_name', 'username', 'email']) + order_by = getattr( + settings, + "EXPORT_EMAILS_ORDER_BY", + ["last_name", "first_name", "username", "email"], + ) + fields = getattr( + settings, + "EXPORT_EMAILS_FIELDS", + ["last_name", "first_name", "username", "email"], + ) qs = UserModel.objects.all().order_by(*order_by) if group: qs = qs.filter(groups__name=group).distinct() qs = qs.values(*fields) - getattr(self, options['format'])(qs) + getattr(self, options["format"])(qs) def address(self, qs): """ Single entry per line in the format of: "full name" ; """ - self.stdout.write("\n".join('"%s" <%s>;' % (self.full_name(**ent), ent.get('email', '')) for ent in qs)) + self.stdout.write( + "\n".join( + '"%s" <%s>;' % (self.full_name(**ent), ent.get("email", "")) + for ent in qs + ) + ) self.stdout.write("\n") def emails(self, qs): @@ -100,15 +124,15 @@ def emails(self, qs): Single entry with email only in the format of: my@address.com, """ - self.stdout.write(",\n".join(ent['email'] for ent in qs if ent.get('email'))) + self.stdout.write(",\n".join(ent["email"] for ent in qs if ent.get("email"))) self.stdout.write("\n") def google(self, qs): """CSV format suitable for importing into google GMail""" csvf = csv.writer(sys.stdout) - csvf.writerow(['Name', 'Email']) + csvf.writerow(["Name", "Email"]) for ent in qs: - csvf.writerow([self.full_name(**ent), ent.get('email', '')]) + csvf.writerow([self.full_name(**ent), ent.get("email", "")]) def linkedin(self, qs): """ @@ -116,42 +140,72 @@ def linkedin(self, qs): perfect for pre-approving members of a linkedin group. """ csvf = csv.writer(sys.stdout) - csvf.writerow(['First Name', 'Last Name', 'Email']) + csvf.writerow(["First Name", "Last Name", "Email"]) for ent in qs: - csvf.writerow([ent.get('first_name', ''), ent.get('last_name', ''), ent.get('email', '')]) + csvf.writerow( + [ + ent.get("first_name", ""), + ent.get("last_name", ""), + ent.get("email", ""), + ] + ) def outlook(self, qs): """CSV format suitable for importing into outlook""" csvf = csv.writer(sys.stdout) - columns = ['Name', 'E-mail Address', 'Notes', 'E-mail 2 Address', 'E-mail 3 Address', - 'Mobile Phone', 'Pager', 'Company', 'Job Title', 'Home Phone', 'Home Phone 2', - 'Home Fax', 'Home Address', 'Business Phone', 'Business Phone 2', - 'Business Fax', 'Business Address', 'Other Phone', 'Other Fax', 'Other Address'] + columns = [ + "Name", + "E-mail Address", + "Notes", + "E-mail 2 Address", + "E-mail 3 Address", + "Mobile Phone", + "Pager", + "Company", + "Job Title", + "Home Phone", + "Home Phone 2", + "Home Fax", + "Home Address", + "Business Phone", + "Business Phone 2", + "Business Fax", + "Business Address", + "Other Phone", + "Other Fax", + "Other Address", + ] csvf.writerow(columns) - empty = [''] * (len(columns) - 2) + empty = [""] * (len(columns) - 2) for ent in qs: - csvf.writerow([self.full_name(**ent), ent.get('email', '')] + empty) + csvf.writerow([self.full_name(**ent), ent.get("email", "")] + empty) def vcard(self, qs): """VCARD format.""" try: import vobject except ImportError: - print(self.style.ERROR("Please install vobject to use the vcard export format.")) + print( + self.style.ERROR( + "Please install vobject to use the vcard export format." + ) + ) sys.exit(1) out = sys.stdout for ent in qs: card = vobject.vCard() - card.add('fn').value = self.full_name(**ent) - if ent.get('last_name') and ent.get('first_name'): - card.add('n').value = vobject.vcard.Name(ent['last_name'], ent['first_name']) + card.add("fn").value = self.full_name(**ent) + if ent.get("last_name") and ent.get("first_name"): + card.add("n").value = vobject.vcard.Name( + ent["last_name"], ent["first_name"] + ) else: # fallback to fullname, if both first and lastname are not declared - card.add('n').value = vobject.vcard.Name(self.full_name(**ent)) - if ent.get('email'): - emailpart = card.add('email') - emailpart.value = ent['email'] - emailpart.type_param = 'INTERNET' + card.add("n").value = vobject.vcard.Name(self.full_name(**ent)) + if ent.get("email"): + emailpart = card.add("email") + emailpart.value = ent["email"] + emailpart.type_param = "INTERNET" out.write(card.serialize()) diff --git a/django_extensions/management/commands/find_template.py b/django_extensions/management/commands/find_template.py index 53bd4a439..f48d3ac57 100644 --- a/django_extensions/management/commands/find_template.py +++ b/django_extensions/management/commands/find_template.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import sys from django.core.management.base import LabelCommand @@ -10,7 +9,7 @@ class Command(LabelCommand): help = "Finds the location of the given template by resolving its path" args = "[template_path]" - label = 'template path' + label = "template path" @signalcommand def handle_label(self, template_path, **options): diff --git a/django_extensions/management/commands/generate_password.py b/django_extensions/management/commands/generate_password.py index e52369484..073a3a15e 100644 --- a/django_extensions/management/commands/generate_password.py +++ b/django_extensions/management/commands/generate_password.py @@ -1,28 +1,35 @@ -# -*- coding: utf-8 -*- -try: - from django.contrib.auth.base_user import BaseUserManager -except ImportError: - from django.contrib.auth.models import BaseUserManager +import argparse +import string +import secrets +from typing import List + from django.core.management.base import BaseCommand from django_extensions.management.utils import signalcommand 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()`." + help = "Generates a simple new password that can be used for a user password. " + "Uses Python’s secrets module to generate passwords. Do not use this command to " + "generate your most secure passwords." - requires_system_checks = False + requires_system_checks: List[str] = [] def add_arguments(self, parser): parser.add_argument( - '--length', nargs='?', type=int, - help='Password length.') + "-l", "--length", nargs="?", type=int, default=16, help="Password length." + ) + parser.add_argument( + "-c", + "--complex", + action=argparse.BooleanOptionalAction, + help="More complex alphabet, includes punctuation", + ) @signalcommand def handle(self, *args, **options): - length = options['length'] - manager = BaseUserManager() + length = options["length"] - if length: - return manager.make_random_password(length) - else: - return manager.make_random_password() + alphabet = string.ascii_letters + string.digits + if options["complex"]: + alphabet += string.punctuation + return "".join(secrets.choice(alphabet) for i in range(length)) diff --git a/django_extensions/management/commands/generate_secret_key.py b/django_extensions/management/commands/generate_secret_key.py index f48232440..14e294bb3 100644 --- a/django_extensions/management/commands/generate_secret_key.py +++ b/django_extensions/management/commands/generate_secret_key.py @@ -1,4 +1,5 @@ -# -*- 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 +9,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/graph_models.py b/django_extensions/management/commands/graph_models.py index c70e16968..e0bfc2f12 100644 --- a/django_extensions/management/commands/graph_models.py +++ b/django_extensions/management/commands/graph_models.py @@ -1,8 +1,9 @@ -# -*- coding: utf-8 -*- import sys import json import os import tempfile +import fnmatch +from collections import OrderedDict from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -13,6 +14,7 @@ try: import pygraphviz + HAS_PYGRAPHVIZ = True except ImportError: HAS_PYGRAPHVIZ = False @@ -26,9 +28,28 @@ except ImportError: HAS_PYDOT = False +DEFAULT_APP_STYLE_NAME = ".app-style.json" + + +def retheme(graph_data: dict, app_style_filename: str): + with open(app_style_filename, "rt") as f: + app_style = json.load(f, object_pairs_hook=OrderedDict) + + for gc in graph_data["graphs"]: + for g in gc: + if "name" in g: + for m in g["models"]: + app_name = g["app_name"] + for pattern, style in app_style.items(): + if fnmatch.fnmatchcase(app_name, pattern): + m["style"] = dict(style) + return graph_data + class Command(BaseCommand): - help = "Creates a GraphViz dot file for the specified app names. You can pass multiple app names and they will all be combined into a single model. Output is usually directed to a dot file." + help = "Creates a GraphViz dot file for the specified app names." + " You can pass multiple app names and they will all be combined into a" + " single model. Output is usually directed to a dot file." can_import_settings = True @@ -44,165 +65,230 @@ def __init__(self, *args, **kwargs): --disable-fields can be set in settings.GRAPH_MODELS['disable_fields']. """ self.arguments = { - '--pygraphviz': { - 'action': 'store_true', - 'default': False, - 'dest': 'pygraphviz', - 'help': 'Output graph data as image using PyGraphViz.', + "--app-style": { + "action": "store", + "help": "Path to style json to configure the style per app", + "dest": "app-style", + "default": "", + }, + "--pygraphviz": { + "action": "store_true", + "default": False, + "dest": "pygraphviz", + "help": "Output graph data as image using PyGraphViz.", + }, + "--pydot": { + "action": "store_true", + "default": False, + "dest": "pydot", + "help": "Output graph data as image using PyDot(Plus).", }, - '--pydot': { - 'action': 'store_true', - 'default': False, - 'dest': 'pydot', - 'help': 'Output graph data as image using PyDot(Plus).', + "--dot": { + "action": "store_true", + "default": False, + "dest": "dot", + "help": ( + "Output graph data as raw DOT (graph description language) " + "text data." + ), }, - '--dot': { - 'action': 'store_true', - 'default': False, - 'dest': 'dot', - 'help': 'Output graph data as raw DOT (graph description language) text data.', + "--json": { + "action": "store_true", + "default": False, + "dest": "json", + "help": "Output graph data as JSON", }, - '--json': { - 'action': 'store_true', - 'default': False, - 'dest': 'json', - 'help': 'Output graph data as JSON', + "--disable-fields -d": { + "action": "store_true", + "default": False, + "dest": "disable_fields", + "help": "Do not show the class member fields", }, - '--disable-fields -d': { - 'action': 'store_true', - 'default': False, - 'dest': 'disable_fields', - 'help': 'Do not show the class member fields', + "--disable-abstract-fields": { + "action": "store_true", + "default": False, + "dest": "disable_abstract_fields", + "help": "Do not show the class member fields that were inherited", }, - '--disable-abstract-fields': { - 'action': 'store_true', - 'default': False, - 'dest': 'disable_abstract_fields', - 'help': 'Do not show the class member fields that were inherited', + "--display-field-choices": { + "action": "store_true", + "default": False, + "dest": "display_field_choices", + "help": "Display choices instead of field type", }, - '--group-models -g': { - 'action': 'store_true', - 'default': False, - 'dest': 'group_models', - 'help': 'Group models together respective to their application', + "--group-models -g": { + "action": "store_true", + "default": False, + "dest": "group_models", + "help": "Group models together respective to their application", }, - '--all-applications -a': { - 'action': 'store_true', - 'default': False, - 'dest': 'all_applications', - 'help': 'Automatically include all applications from INSTALLED_APPS', + "--all-applications -a": { + "action": "store_true", + "default": False, + "dest": "all_applications", + "help": "Automatically include all applications from INSTALLED_APPS", }, - '--output -o': { - 'action': 'store', - 'dest': 'outputfile', - 'help': 'Render output file. Type of output dependend on file extensions. Use png or jpg to render graph to image.', + "--output -o": { + "action": "store", + "dest": "outputfile", + "help": ( + "Render output file. Type of output dependend on file extensions. " + "Use png or jpg to render graph to image." + ), }, - '--layout -l': { - 'action': 'store', - 'dest': 'layout', - 'default': 'dot', - 'help': 'Layout to be used by GraphViz for visualization. Layouts: circo dot fdp neato nop nop1 nop2 twopi', + "--layout -l": { + "action": "store", + "dest": "layout", + "default": "dot", + "help": "Layout to be used by GraphViz for visualization. Layouts: " + "circo dot fdp neato nop nop1 nop2 twopi", }, - '--theme -t': { - 'action': 'store', - 'dest': 'theme', - 'default': 'django2018', - 'help': 'Theme to use. Supplied are \'original\' and \'django2018\'. You can create your own by creating dot templates in \'django_extentions/graph_models/themename/\' template directory.', + "--theme -t": { + "action": "store", + "dest": "theme", + "default": "django2018", + "help": "Theme to use. Supplied are 'original' and 'django2018'. " + "You can create your own by creating dot templates in " + "'django_extentions/graph_models/themename/' template directory.", }, - '--verbose-names -n': { - 'action': 'store_true', - 'default': False, - 'dest': 'verbose_names', - 'help': 'Use verbose_name of models and fields', + "--verbose-names -n": { + "action": "store_true", + "default": False, + "dest": "verbose_names", + "help": "Use verbose_name of models and fields", }, - '--language -L': { - 'action': 'store', - 'dest': 'language', - 'help': 'Specify language used for verbose_name localization', + "--language -L": { + "action": "store", + "dest": "language", + "help": "Specify language used for verbose_name localization", }, - '--exclude-columns -x': { - 'action': 'store', - 'dest': 'exclude_columns', - 'help': 'Exclude specific column(s) from the graph. Can also load exclude list from file.', + "--exclude-columns -x": { + "action": "store", + "dest": "exclude_columns", + "help": "Exclude specific column(s) from the graph. " + "Can also load exclude list from file.", }, - '--exclude-models -X': { - 'action': 'store', - 'dest': 'exclude_models', - 'help': 'Exclude specific model(s) from the graph. Can also load exclude list from file. Wildcards (*) are allowed.', + "--exclude-models -X": { + "action": "store", + "dest": "exclude_models", + "help": "Exclude specific model(s) from the graph. Can also load " + "exclude list from file. Wildcards (*) are allowed.", }, - '--include-models -I': { - 'action': 'store', - 'dest': 'include_models', - 'help': 'Restrict the graph to specified models. Wildcards (*) are allowed.', + "--include-models -I": { + "action": "store", + "dest": "include_models", + "help": "Restrict the graph to specified models. " + "Wildcards (*) are allowed.", }, - '--inheritance -e': { - 'action': 'store_true', - 'default': True, - 'dest': 'inheritance', - 'help': 'Include inheritance arrows (default)', + "--inheritance -e": { + "action": "store_true", + "default": True, + "dest": "inheritance", + "help": "Include inheritance arrows (default)", }, - '--no-inheritance -E': { - 'action': 'store_false', - 'default': False, - 'dest': 'inheritance', - 'help': 'Do not include inheritance arrows', + "--no-inheritance -E": { + "action": "store_false", + "default": False, + "dest": "inheritance", + "help": "Do not include inheritance arrows", }, - '--hide-relations-from-fields -R': { - 'action': 'store_false', - 'default': True, - 'dest': 'relations_as_fields', - 'help': 'Do not show relations as fields in the graph.', + "--hide-relations-from-fields -R": { + "action": "store_false", + "default": True, + "dest": "relations_as_fields", + "help": "Do not show relations as fields in the graph.", }, - '--disable-sort-fields -S': { - 'action': 'store_false', - 'default': True, - 'dest': 'sort_fields', - 'help': 'Do not sort fields', + "--relation-fields-only": { + "action": "store", + "default": False, + "dest": "relation_fields_only", + "help": "Only display fields that are relevant for relations", }, - '--hide-edge-labels': { - 'action': 'store_true', - 'default': False, - 'dest': 'hide_edge_labels', - 'help': 'Do not showrelations labels in the graph.', + "--disable-sort-fields -S": { + "action": "store_false", + "default": True, + "dest": "sort_fields", + "help": "Do not sort fields", }, - '--arrow-shape': { - 'action': 'store', - 'default': 'dot', - '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.', + "--hide-edge-labels": { + "action": "store_true", + "default": False, + "dest": "hide_edge_labels", + "help": "Do not show relations labels in the graph.", }, - '--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.' + "--arrow-shape": { + "action": "store", + "default": "dot", + "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.", + }, + "--color-code-deletions": { + "action": "store_true", + "default": False, + "dest": "color_code_deletions", + "help": "Color the relations according to their on_delete setting, " + "where it is applicable. The colors are: red (CASCADE), " + "orange (SET_NULL), green (SET_DEFAULT), yellow (SET), " + "blue (PROTECT), grey (DO_NOTHING), and purple (RESTRICT).", + }, + "--rankdir": { + "action": "store", + "default": "TB", + "dest": "rankdir", + "choices": ["TB", "BT", "LR", "RL"], + "help": "Set direction of graph layout. Supported directions: " + "TB, LR, BT and 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.", + }, + "--ordering": { + "action": "store", + "default": None, + "dest": "ordering", + "choices": ["in", "out"], + "help": "Controls how the edges are arranged. Supported orderings: " + '"in" (incoming relations first), "out" (outgoing relations first). ' + "Default is None.", }, } - defaults = getattr(settings, 'GRAPH_MODELS', None) + defaults = getattr(settings, "GRAPH_MODELS", None) if defaults: for argument in self.arguments: - arg_split = argument.split(' ') - setting_opt = arg_split[0].lstrip('-').replace('-', '_') + arg_split = argument.split(" ") + setting_opt = arg_split[0].lstrip("-").replace("-", "_") if setting_opt in defaults: - self.arguments[argument]['default'] = defaults[setting_opt] + self.arguments[argument]["default"] = defaults[setting_opt] super().__init__(*args, **kwargs) def add_arguments(self, parser): """Unpack self.arguments for parser.add_arguments.""" - parser.add_argument('app_label', nargs='*') + parser.add_argument("app_label", nargs="*") for argument in self.arguments: - parser.add_argument(*argument.split(' '), **self.arguments[argument]) + parser.add_argument(*argument.split(" "), **self.arguments[argument]) @signalcommand def handle(self, *args, **options): - args = options['app_label'] - if not args and not options['all_applications']: - default_app_labels = getattr(settings, 'GRAPH_MODELS', {}).get("app_labels") + args = options["app_label"] + if not args and not options["all_applications"]: + default_app_labels = getattr(settings, "GRAPH_MODELS", {}).get("app_labels") if default_app_labels: args = default_app_labels else: @@ -213,11 +299,14 @@ def handle(self, *args, **options): outputfile = options.get("outputfile") or "" _, outputfile_ext = os.path.splitext(outputfile) outputfile_ext = outputfile_ext.lower() - output_opts_names = ['pydot', 'pygraphviz', 'json', 'dot'] + output_opts_names = ["pydot", "pygraphviz", "json", "dot"] output_opts = {k: v for k, v in options.items() if k in output_opts_names} output_opts_count = sum(output_opts.values()) if output_opts_count > 1: - raise CommandError("Only one of %s can be set." % ", ".join(["--%s" % opt for opt in output_opts_names])) + raise CommandError( + "Only one of %s can be set." + % ", ".join(["--%s" % opt for opt in output_opts_names]) + ) if output_opts_count == 1: output = next(key for key, val in output_opts.items() if val) @@ -235,18 +324,35 @@ def handle(self, *args, **options): elif HAS_PYDOT: output = "pydot" 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") + 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" + ) + + if options.get("ordering") and output not in ["pydot", "pygraphviz", "dot"]: + raise CommandError( + "--ordering 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. if output in ["pydot", "pygraphviz"] and not outputfile: - raise CommandError("An output file (--output) must be specified when --pydot or --pygraphviz are set.") + raise CommandError( + "An output file (--output) must be specified when --pydot or " + "--pygraphviz are set." + ) - cli_options = ' '.join(sys.argv[2:]) + cli_options = " ".join(sys.argv[2:]) graph_models = ModelGraph(args, cli_options=cli_options, **options) graph_models.generate_graph_data() @@ -256,10 +362,27 @@ def handle(self, *args, **options): graph_data = graph_models.get_graph_data(as_json=False) - theme = options['theme'] - template_name = os.path.join('django_extensions', 'graph_models', theme, 'digraph.dot') + theme = options["theme"] + template_name = os.path.join( + "django_extensions", "graph_models", theme, "digraph.dot" + ) template = loader.get_template(template_name) + app_style_filename = options["app-style"] + if app_style_filename and not os.path.exists(app_style_filename): + raise CommandError(f"--app-style file {app_style_filename} not found") + + if not app_style_filename: + # try default + default_app_style_filename = os.path.join( + settings.BASE_DIR, DEFAULT_APP_STYLE_NAME + ) + if os.path.exists(default_app_style_filename): + app_style_filename = default_app_style_filename + + if app_style_filename: + graph_data = retheme(graph_data, app_style_filename=app_style_filename) + dotdata = generate_dot(graph_data, template=template) if output == "pygraphviz": @@ -274,7 +397,7 @@ def print_output(self, dotdata, output_file=None): dotdata = dotdata.decode() if output_file: - with open(output_file, 'wt') as dot_output_f: + with open(output_file, "wt") as dot_output_f: dot_output_f.write(dotdata) else: self.stdout.write(dotdata) @@ -282,7 +405,7 @@ def print_output(self, dotdata, output_file=None): def render_output_json(self, graph_data, output_file=None): """Write model data to file or stdout in JSON format.""" if output_file: - with open(output_file, 'wt') as json_output_f: + with open(output_file, "wt") as json_output_f: json.dump(graph_data, json_output_f) else: self.stdout.write(json.dumps(graph_data)) @@ -294,8 +417,9 @@ def render_output_pygraphviz(self, dotdata, **kwargs): version = pygraphviz.__version__.rstrip("-svn") try: - if tuple(int(v) for v in version.split('.')) < (0, 36): - # HACK around old/broken AGraph before version 0.36 (ubuntu ships with this old version) + if tuple(int(v) for v in version.split(".")) < (0, 36): + # HACK around old/broken AGraph before version 0.36 + # (ubuntu ships with this old version) tmpfile = tempfile.NamedTemporaryFile() tmpfile.write(dotdata) tmpfile.seek(0) @@ -304,8 +428,8 @@ def render_output_pygraphviz(self, dotdata, **kwargs): pass graph = pygraphviz.AGraph(dotdata) - graph.layout(prog=kwargs['layout']) - graph.draw(kwargs['outputfile']) + graph.layout(prog=kwargs["layout"]) + graph.draw(kwargs["outputfile"]) def render_output_pydot(self, dotdata, **kwargs): """Render model data as image using pydot.""" @@ -317,17 +441,56 @@ def render_output_pydot(self, dotdata, **kwargs): raise CommandError("pydot returned an error") if isinstance(graph, (list, tuple)): if len(graph) > 1: - sys.stderr.write("Found more then one graph, rendering only the first one.\n") + sys.stderr.write( + "Found more then one graph, rendering only the first one.\n" + ) graph = graph[0] - output_file = kwargs['outputfile'] + output_file = kwargs["outputfile"] formats = [ - 'bmp', 'canon', 'cmap', 'cmapx', 'cmapx_np', 'dot', 'dia', 'emf', - 'em', 'fplus', 'eps', 'fig', 'gd', 'gd2', 'gif', 'gv', 'imap', - 'imap_np', 'ismap', 'jpe', 'jpeg', 'jpg', 'metafile', 'pdf', - 'pic', 'plain', 'plain-ext', 'png', 'pov', 'ps', 'ps2', 'svg', - 'svgz', 'tif', 'tiff', 'tk', 'vml', 'vmlz', 'vrml', 'wbmp', 'xdot', + "bmp", + "canon", + "cmap", + "cmapx", + "cmapx_np", + "dot", + "dia", + "emf", + "em", + "fplus", + "eps", + "fig", + "gd", + "gd2", + "gif", + "gv", + "imap", + "imap_np", + "ismap", + "jpe", + "jpeg", + "jpg", + "metafile", + "pdf", + "pic", + "plain", + "plain-ext", + "png", + "pov", + "ps", + "ps2", + "svg", + "svgz", + "tif", + "tiff", + "tk", + "vml", + "vmlz", + "vrml", + "wbmp", + "webp", + "xdot", ] - ext = output_file[output_file.rfind('.') + 1:] - format_ = ext if ext in formats else 'raw' + ext = output_file[output_file.rfind(".") + 1 :] + format_ = ext if ext in formats else "raw" graph.write(output_file, format=format_) diff --git a/django_extensions/management/commands/list_model_info.py b/django_extensions/management/commands/list_model_info.py index 15e067f0e..286711020 100644 --- a/django_extensions/management/commands/list_model_info.py +++ b/django_extensions/management/commands/list_model_info.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Author: OmenApps. http://www.omenapps.com +# Author: OmenApps. https://omenapps.com import inspect from django.apps import apps as django_apps @@ -20,36 +19,64 @@ class Command(BaseCommand): def add_arguments(self, parser): super().add_arguments(parser) - parser.add_argument("--field-class", action="store_true", default=None, help="show class name of field.") - parser.add_argument("--db-type", action="store_true", default=None, help="show database column type of field.") - parser.add_argument("--signature", action="store_true", default=None, help="show the signature of method.") parser.add_argument( - "--all-methods", action="store_true", default=None, help="list all methods, including private and default." + "--field-class", + action="store_true", + default=None, + help="show class name of field.", + ) + parser.add_argument( + "--db-type", + action="store_true", + default=None, + help="show database column type of field.", + ) + parser.add_argument( + "--signature", + action="store_true", + default=None, + help="show the signature of method.", + ) + parser.add_argument( + "--all-methods", + action="store_true", + default=None, + help="list all methods, including private and default.", ) parser.add_argument( "--model", nargs="?", type=str, default=None, - help="list the details for a single model. Input should be in the form appname.Modelname", + help="list the details for a single model. " + "Input should be in the form appname.Modelname", ) def list_model_info(self, options): - style = color_style() INFO = getattr(style, "INFO", lambda x: x) WARN = getattr(style, "WARN", lambda x: x) BOLD = getattr(style, "BOLD", lambda x: x) FIELD_CLASS = ( - True if options.get("field_class", None) is not None else getattr(settings, "MODEL_INFO_FIELD_CLASS", False) + True + if options.get("field_class", None) is not None + else getattr(settings, "MODEL_INFO_FIELD_CLASS", False) + ) + DB_TYPE = ( + True + if options.get("db_type", None) is not None + else getattr(settings, "MODEL_INFO_DB_TYPE", False) ) - DB_TYPE = True if options.get("db_type", None) is not None else getattr(settings, "MODEL_INFO_DB_TYPE", False) SIGNATURE = ( - True if options.get("signature", None) is not None else getattr(settings, "MODEL_INFO_SIGNATURE", False) + True + if options.get("signature", None) is not None + else getattr(settings, "MODEL_INFO_SIGNATURE", False) ) ALL_METHODS = ( - True if options.get("all_methods", None) is not None else getattr(settings, "MODEL_INFO_ALL_METHODS", False) + True + if options.get("all_methods", None) is not None + else getattr(settings, "MODEL_INFO_ALL_METHODS", False) ) MODEL = ( options.get("model") @@ -58,6 +85,9 @@ def list_model_info(self, options): ) default_methods = [ + "adelete", + "arefresh_from_db", + "asave", "check", "clean", "clean_fields", @@ -66,6 +96,7 @@ def list_model_info(self, options): "from_db", "full_clean", "get_absolute_url", + "get_constraints", "get_deferred_fields", "prepare_database_save", "refresh_from_db", @@ -73,6 +104,7 @@ def list_model_info(self, options): "save_base", "serializable_value", "unique_error_message", + "validate_constraints", "validate_unique", ] @@ -80,10 +112,14 @@ def list_model_info(self, options): model_list = [django_apps.get_model(MODEL)] else: model_list = sorted( - django_apps.get_models(), key=lambda x: (x._meta.app_label, x._meta.object_name), reverse=False + django_apps.get_models(), + key=lambda x: (x._meta.app_label, x._meta.object_name), + reverse=False, ) for model in model_list: - self.stdout.write(INFO(model._meta.app_label + "." + model._meta.object_name)) + self.stdout.write( + INFO(model._meta.app_label + "." + model._meta.object_name) + ) self.stdout.write(BOLD(HALFTAB + "Fields:")) for field in model._meta.get_fields(): @@ -93,18 +129,18 @@ def list_model_info(self, options): try: field_info += " " + field.__class__.__name__ except TypeError: - field_info += (WARN(" TypeError (field_class)")) + field_info += WARN(" TypeError (field_class)") except AttributeError: - field_info += (WARN(" AttributeError (field_class)")) + field_info += WARN(" AttributeError (field_class)") if FIELD_CLASS and DB_TYPE: field_info += "," if DB_TYPE: try: field_info += " " + field.db_type(connection=connection) except TypeError: - field_info += (WARN(" TypeError (db_type)")) + field_info += WARN(" TypeError (db_type)") except AttributeError: - field_info += (WARN(" AttributeError (db_type)")) + field_info += WARN(" AttributeError (db_type)") self.stdout.write(field_info) @@ -138,7 +174,11 @@ def list_model_info(self, options): except AttributeError: self.stdout.write(TAB + method_name + WARN(" - AttributeError")) except ValueError: - self.stdout.write(TAB + method_name + WARN(" - ValueError (could not identify signature)")) + self.stdout.write( + TAB + + method_name + + WARN(" - ValueError (could not identify signature)") + ) self.stdout.write("\n") diff --git a/django_extensions/management/commands/list_signals.py b/django_extensions/management/commands/list_signals.py index c41aa6f14..9cadc4f85 100644 --- a/django_extensions/management/commands/list_signals.py +++ b/django_extensions/management/commands/list_signals.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Based on https://gist.github.com/voldmar/1264102 # and https://gist.github.com/runekaagaard/2eecf0a8367959dc634b7866694daf2c @@ -7,32 +6,41 @@ import weakref from collections import defaultdict +import django from django.apps import apps from django.core.management.base import BaseCommand from django.db.models.signals import ( - ModelSignal, pre_init, post_init, pre_save, post_save, pre_delete, - post_delete, m2m_changed, pre_migrate, post_migrate + ModelSignal, + pre_init, + post_init, + pre_save, + post_save, + pre_delete, + post_delete, + m2m_changed, + pre_migrate, + post_migrate, ) from django.utils.encoding import force_str -MSG = '{module}.{name} #{line}' +MSG = "{module}.{name} #{line}{is_async}" SIGNAL_NAMES = { - pre_init: 'pre_init', - post_init: 'post_init', - pre_save: 'pre_save', - post_save: 'post_save', - pre_delete: 'pre_delete', - post_delete: 'post_delete', - m2m_changed: 'm2m_changed', - pre_migrate: 'pre_migrate', - post_migrate: 'post_migrate', + pre_init: "pre_init", + post_init: "post_init", + pre_save: "pre_save", + post_save: "post_save", + pre_delete: "pre_delete", + post_delete: "post_delete", + m2m_changed: "m2m_changed", + pre_migrate: "pre_migrate", + post_migrate: "post_migrate", } class Command(BaseCommand): - help = 'List all signals by model and signal type' + help = "List all signals by model and signal type" def handle(self, *args, **options): all_models = apps.get_models(include_auto_created=True, include_swapped=True) @@ -42,33 +50,41 @@ def handle(self, *args, **options): models = defaultdict(lambda: defaultdict(list)) for signal in signals: - signal_name = SIGNAL_NAMES.get(signal, 'unknown') + signal_name = SIGNAL_NAMES.get(signal, "unknown") for receiver in signal.receivers: - lookup, receiver = receiver + if django.VERSION >= (5, 0): + lookup, receiver, is_async = receiver + else: + lookup, receiver = receiver + is_async = False if isinstance(receiver, weakref.ReferenceType): receiver = receiver() if receiver is None: continue receiver_id, sender_id = lookup - model = model_lookup.get(sender_id, '_unknown_') + model = model_lookup.get(sender_id, "_unknown_") if model: - models[model][signal_name].append(MSG.format( - name=receiver.__name__, - module=receiver.__module__, - line=inspect.getsourcelines(receiver)[1], - path=inspect.getsourcefile(receiver)) + models[model][signal_name].append( + MSG.format( + name=receiver.__name__, + module=receiver.__module__, + is_async=" (async)" if is_async else "", + line=inspect.getsourcelines(receiver)[1], + path=inspect.getsourcefile(receiver), + ) ) output = [] for key in sorted(models.keys(), key=str): verbose_name = force_str(key._meta.verbose_name) - output.append('{}.{} ({})'.format( - key.__module__, key.__name__, verbose_name)) + output.append( + "{}.{} ({})".format(key.__module__, key.__name__, verbose_name) + ) for signal_name in sorted(models[key].keys()): lines = models[key][signal_name] - output.append(' {}'.format(signal_name)) + output.append(" {}".format(signal_name)) for line in lines: - output.append(' {}'.format(line)) + output.append(" {}".format(line)) - return '\n'.join(output) + return "\n".join(output) diff --git a/django_extensions/management/commands/mail_debug.py b/django_extensions/management/commands/mail_debug.py index 4bf6b0ba6..f12bbd221 100644 --- a/django_extensions/management/commands/mail_debug.py +++ b/django_extensions/management/commands/mail_debug.py @@ -1,8 +1,13 @@ -# -*- coding: utf-8 -*- -import asyncore +import asyncio import sys + +try: + from aiosmtpd.controller import Controller +except ImportError: + raise ImportError("Please install 'aiosmtpd' library to use mail_debug command.") + from logging import getLogger -from smtpd import SMTPServer +from typing import List from django.core.management.base import BaseCommand, CommandError @@ -11,60 +16,65 @@ logger = getLogger(__name__) -class ExtensionDebuggingServer(SMTPServer): - """Duplication of smtpd.DebuggingServer, but using logging instead of print.""" - - # Do something with the gathered message - def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): +class CustomHandler: + async def handle_DATA(self, server, session, envelope): """Output will be sent to the module logger at INFO level.""" + peer = session.peer inheaders = 1 - lines = data.split('\n') - logger.info('---------- MESSAGE FOLLOWS ----------') + lines = envelope.content.decode("utf8", errors="replace").splitlines() + logger.info("---------- MESSAGE FOLLOWS ----------") for line in lines: # headers first if inheaders and not line: - logger.info('X-Peer: %s' % peer[0]) + logger.info("X-Peer: %s" % peer[0]) inheaders = 0 logger.info(line) - logger.info('------------ END MESSAGE ------------') + logger.info("------------ END MESSAGE ------------") + return "250 OK" class Command(BaseCommand): help = "Starts a test mail server for development." - args = '[optional port number or ippaddr:port]' + 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) - parser.add_argument('addrport', nargs='?') + parser.add_argument("addrport", nargs="?") parser.add_argument( - '--output', dest='output_file', default=None, - help='Specifies an output file to send a copy of all messages (not flushed immediately).' + "--output", + dest="output_file", + default=None, + help="Specifies an output file to send a copy of all messages " + "(not flushed immediately).", ) parser.add_argument( - '--use-settings', dest='use_settings', - action='store_true', default=False, - help='Uses EMAIL_HOST and HOST_PORT from Django settings.' + "--use-settings", + dest="use_settings", + action="store_true", + default=False, + help="Uses EMAIL_HOST and HOST_PORT from Django settings.", ) @signalcommand - def handle(self, addrport='', *args, **options): + def handle(self, addrport="", *args, **options): if not addrport: - if options['use_settings']: + if options["use_settings"]: from django.conf import settings - addr = getattr(settings, 'EMAIL_HOST', '') - port = str(getattr(settings, 'EMAIL_PORT', '1025')) + + addr = getattr(settings, "EMAIL_HOST", "") + port = str(getattr(settings, "EMAIL_PORT", "1025")) else: - addr = '' - port = '1025' + addr = "" + port = "1025" else: try: - addr, port = addrport.split(':') + addr, port = addrport.split(":") except ValueError: - addr, port = '', addrport + addr, port = "", addrport if not addr: - addr = '127.0.0.1' + addr = "127.0.0.1" if not port.isdigit(): raise CommandError("%r is not a valid port number." % port) @@ -72,13 +82,19 @@ def handle(self, addrport='', *args, **options): port = int(port) # Add console handler - setup_logger(logger, stream=self.stdout, filename=options['output_file']) + setup_logger(logger, stream=self.stdout, filename=options["output_file"]) def inner_run(): - quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C' - print("Now accepting mail at %s:%s -- use %s to quit" % (addr, port, quit_command)) - ExtensionDebuggingServer((addr, port), None, decode_data=True) - asyncore.loop() + quit_command = (sys.platform == "win32") and "CTRL-BREAK" or "CONTROL-C" + print( + "Now accepting mail at %s:%s -- use %s to quit" + % (addr, port, quit_command) + ) + handler = CustomHandler() + controller = Controller(handler, hostname=addr, port=port) + controller.start() + loop = asyncio.get_event_loop() + loop.run_forever() try: inner_run() diff --git a/django_extensions/management/commands/managestate.py b/django_extensions/management/commands/managestate.py new file mode 100644 index 000000000..e7ff244fd --- /dev/null +++ b/django_extensions/management/commands/managestate.py @@ -0,0 +1,203 @@ +import json +from operator import itemgetter +from pathlib import Path + +from django.core.management import call_command +from django.core.management.base import BaseCommand, CommandError +from django.db import DEFAULT_DB_ALIAS, connections +from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.migrations.loader import MigrationLoader +from django.db.migrations.recorder import MigrationRecorder +from django.utils import timezone + +from django_extensions.management.utils import signalcommand + +DEFAULT_FILENAME = "managestate.json" +DEFAULT_STATE = "default" + + +class Command(BaseCommand): + help = "Manage database state in the convenient way." + _applied_migrations = None + migrate_args: dict + migrate_options: dict + filename: str + verbosity: int + database: str + conn: BaseDatabaseWrapper + + def add_arguments(self, parser): + parser.add_argument( + "action", + choices=("dump", "load"), + help="An action to do. " + "Dump action saves applied migrations to a file. " + "Load action applies migrations specified in a file.", + ) + parser.add_argument( + "state", + nargs="?", + default=DEFAULT_STATE, + help="A name of a state. Usually a name of a git branch." + f'Defaults to "{DEFAULT_STATE}"', + ) + parser.add_argument( + "-d", + "--database", + default=DEFAULT_DB_ALIAS, + help="Nominates a database to synchronize. " + f'Defaults to the "{DEFAULT_DB_ALIAS}" database.', + ) + parser.add_argument( + "-f", + "--filename", + default=DEFAULT_FILENAME, + help=f'A file to write to. Defaults to "{DEFAULT_FILENAME}"', + ) + + # migrate command arguments + parser.add_argument( + "--noinput", + "--no-input", + action="store_false", + dest="interactive", + help='The argument for "migrate" command. ' + "Tells Django to NOT prompt the user for input of any kind.", + ) + parser.add_argument( + "--fake", + action="store_true", + help='The argument for "migrate" command. ' + "Mark migrations as run without actually running them.", + ) + parser.add_argument( + "--fake-initial", + action="store_true", + help='The argument for "migrate" command. ' + "Detect if tables already exist and fake-apply initial migrations if so. " + "Make sure that the current database schema matches your initial migration " + "before using this flag. " + "Django will only check for an existing table name.", + ) + parser.add_argument( + "--plan", + action="store_true", + help='The argument for "migrate" command. ' + "Shows a list of the migration actions that will be performed.", + ) + parser.add_argument( + "--run-syncdb", + action="store_true", + help='The argument for "migrate" command. ' + "Creates tables for apps without migrations.", + ) + parser.add_argument( + "--check", + action="store_true", + dest="check_unapplied", + help='The argument for "migrate" command. ' + "Exits with a non-zero status if unapplied migrations exist.", + ) + + @signalcommand + def handle(self, action, database, filename, state, *args, **options): + self.migrate_args = args + self.migrate_options = options + self.verbosity = options["verbosity"] + self.conn = connections[database] + self.database = database + self.filename = filename + getattr(self, action)(state) + + def dump(self, state: str): + """Save applied migrations to a file.""" + migrated_apps = self.get_migrated_apps() + migrated_apps.update(self.get_applied_migrations()) + self.write({state: migrated_apps}) + self.stdout.write( + self.style.SUCCESS( + f'Migrations for state "{state}" have been successfully ' + f"saved to {self.filename}." + ) + ) + + def load(self, state: str): + """Apply migrations from a file.""" + migrations = self.read().get(state) + if migrations is None: + raise CommandError(f"No such state saved: {state}") + + kwargs = { + **self.migrate_options, + "database": self.database, + "verbosity": self.verbosity - 1 if self.verbosity > 1 else 0, + } + + for app, migration in migrations.items(): + if self.is_applied(app, migration): + continue + + if self.verbosity > 1: + self.stdout.write( + self.style.WARNING(f'Applying migrations for "{app}"') + ) + args = (app, migration, *self.migrate_args) + call_command("migrate", *args, **kwargs) + + self.stdout.write( + self.style.SUCCESS( + f'Migrations for "{state}" have been successfully applied.' + ) + ) + + def get_migrated_apps(self) -> dict: + """Installed apps having migrations.""" + apps = MigrationLoader(self.conn).migrated_apps + migrated_apps = dict.fromkeys(apps, "zero") + if self.verbosity > 1: + self.stdout.write( + "Apps having migrations: " + ", ".join(sorted(migrated_apps)) + ) + return migrated_apps + + def get_applied_migrations(self) -> dict: + """Installed apps with last applied migrations.""" + if self._applied_migrations: + return self._applied_migrations + + migrations = MigrationRecorder(self.conn).applied_migrations() + last_applied = sorted(migrations.keys(), key=itemgetter(1)) + + self._applied_migrations = dict(last_applied) + return self._applied_migrations + + def is_applied(self, app: str, migration: str) -> bool: + """Check whether a migration for an app is applied or not.""" + applied = self.get_applied_migrations().get(app) + if applied == migration: + if self.verbosity > 1: + self.stdout.write( + self.style.WARNING(f'Migrations for "{app}" are already applied.') + ) + return True + return False + + def read(self) -> dict: + """Get saved state from the file.""" + path = Path(self.filename) + if not path.exists() or not path.is_file(): + raise CommandError(f"No such file: {self.filename}") + + with open(self.filename) as file: + return json.load(file) + + def write(self, data: dict): + """Write new data to the file using existent one.""" + try: + saved = self.read() + except CommandError: + saved = {} + + saved.update(data, updated_at=str(timezone.now())) + with open(self.filename, "w") as file: + json.dump(saved, file, indent=2, sort_keys=True) diff --git a/django_extensions/management/commands/merge_model_instances.py b/django_extensions/management/commands/merge_model_instances.py index e53afdc14..3860be87e 100644 --- a/django_extensions/management/commands/merge_model_instances.py +++ b/django_extensions/management/commands/merge_model_instances.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.apps import apps from django.contrib.contenttypes.fields import GenericForeignKey from django.core.management import BaseCommand @@ -13,7 +12,9 @@ def get_model_to_deduplicate(): for model in models: print("%s. %s" % (iterator, model.__name__)) iterator += 1 - model_choice = int(input("Enter the number of the model you would like to de-duplicate:")) + model_choice = int( + input("Enter the number of the model you would like to de-duplicate:") + ) model_to_deduplicate = models[model_choice - 1] return model_to_deduplicate @@ -26,7 +27,11 @@ def get_field_names(model): iterator += 1 validated = False while not validated: - first_field = int(input("Enter the number of the (first) field you would like to de-duplicate.")) + first_field = int( + input( + "Enter the number of the (first) field you would like to de-duplicate." + ) + ) if first_field in range(1, iterator): validated = True else: @@ -35,9 +40,7 @@ def get_field_names(model): done = False while not done: - available_fields = [ - f for f in fields if f not in fields_to_deduplicate - ] + available_fields = [f for f in fields if f not in fields_to_deduplicate] iterator = 1 for field in available_fields: print("%s. %s" % (iterator, field)) @@ -47,7 +50,7 @@ def get_field_names(model): validated = False while not validated: print("You are currently deduplicating on the following fields:") - print('\n'.join(fields_to_deduplicate) + '\n') + print("\n".join(fields_to_deduplicate) + "\n") additional_field = input(""" Enter the number of the field you would like to de-duplicate. @@ -109,9 +112,7 @@ def handle(self, *args, **options): kwargs = {} for field_name in field_names: instance_field_value = instance.__getattribute__(field_name) - kwargs.update({ - field_name: instance_field_value - }) + kwargs.update({field_name: instance_field_value}) try: model.objects.get(**kwargs) except model.MultipleObjectsReturned: @@ -123,10 +124,16 @@ def handle(self, *args, **options): primary_object = instances.last() alias_objects = instances.exclude(pk=primary_object.pk) - primary_object, deleted_objects, deleted_objects_count = self.merge_model_instances(primary_object, alias_objects) + primary_object, deleted_objects, deleted_objects_count = ( + self.merge_model_instances(primary_object, alias_objects) + ) total_deleted_objects_count += deleted_objects_count - print("Successfully deleted {} model instances.".format(total_deleted_objects_count)) + print( + "Successfully deleted {} model instances.".format( + total_deleted_objects_count + ) + ) @transaction.atomic() def merge_model_instances(self, primary_object, alias_objects): @@ -138,15 +145,15 @@ def merge_model_instances(self, primary_object, alias_objects): generic_fields = get_generic_fields() # get related fields - related_fields = list(filter( - lambda x: x.is_relation is True, - primary_object._meta.get_fields())) + related_fields = list( + filter(lambda x: x.is_relation is True, primary_object._meta.get_fields()) + ) - many_to_many_fields = list(filter( - lambda x: x.many_to_many is True, related_fields)) + many_to_many_fields = list( + filter(lambda x: x.many_to_many is True, related_fields) + ) - related_fields = list(filter( - lambda x: x.many_to_many is False, related_fields)) + related_fields = list(filter(lambda x: x.many_to_many is False, related_fields)) # Loop through all alias objects and migrate their references to the # primary object @@ -178,7 +185,8 @@ def merge_model_instances(self, primary_object, alias_objects): setattr( instance, many_to_many_field.m2m_field_name(), - primary_object) + primary_object, + ) instance.save() # TODO: Here, try to delete duplicate instances that are # disallowed by a unique_together constraint @@ -199,8 +207,11 @@ def merge_model_instances(self, primary_object, alias_objects): setattr(primary_object, alias_varname, related_object) primary_object.save() elif related_field.one_to_one: - self.stdout.write("Deleted {} with id {}\n".format( - related_object, related_object.id)) + self.stdout.write( + "Deleted {} with id {}\n".format( + related_object, related_object.id + ) + ) related_object.delete() for field in generic_fields: @@ -214,8 +225,9 @@ def merge_model_instances(self, primary_object, alias_objects): if alias_object.id: deleted_objects += [alias_object] - self.stdout.write("Deleted {} with id {}\n".format( - alias_object, alias_object.id)) + self.stdout.write( + "Deleted {} with id {}\n".format(alias_object, alias_object.id) + ) alias_object.delete() deleted_objects_count += 1 diff --git a/django_extensions/management/commands/notes.py b/django_extensions/management/commands/notes.py index 194f37e79..ed133ffb1 100644 --- a/django_extensions/management/commands/notes.py +++ b/django_extensions/management/commands/notes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import re @@ -8,29 +7,35 @@ from django_extensions.compat import get_template_setting from django_extensions.management.utils import signalcommand -ANNOTATION_RE = re.compile(r"\{?#[\s]*?(TODO|FIXME|BUG|HACK|WARNING|NOTE|XXX)[\s:]?(.+)") +ANNOTATION_RE = re.compile( + r"\{?#[\s]*?(TODO|FIXME|BUG|HACK|WARNING|NOTE|XXX)[\s:]?(.+)" +) ANNOTATION_END_RE = re.compile(r"(.*)#\}(.*)") class Command(BaseCommand): - help = 'Show all annotations like TODO, FIXME, BUG, HACK, WARNING, NOTE or XXX in your py and HTML files.' - label = 'annotation tag (TODO, FIXME, BUG, HACK, WARNING, NOTE, XXX)' + help = "Show all annotations like TODO, FIXME, BUG, HACK, WARNING, NOTE or XXX " + "in your py and HTML files." + label = "annotation tag (TODO, FIXME, BUG, HACK, WARNING, NOTE, XXX)" def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--tag', - dest='tag', - help='Search for specific tags only', - action='append' + "--tag", dest="tag", help="Search for specific tags only", action="append" ) @signalcommand def handle(self, *args, **options): # don't add django internal code - apps = [app.replace(".", "/") for app in filter(lambda app: not app.startswith('django.contrib'), settings.INSTALLED_APPS)] - template_dirs = get_template_setting('DIRS', []) - base_dir = getattr(settings, 'BASE_DIR') + apps = [ + app.replace(".", "/") + for app in filter( + lambda app: not app.startswith("django.contrib"), + settings.INSTALLED_APPS, + ) + ] + template_dirs = get_template_setting("DIRS", []) + base_dir = getattr(settings, "BASE_DIR") if template_dirs: apps += template_dirs for app_dir in apps: @@ -38,23 +43,29 @@ def handle(self, *args, **options): app_dir = os.path.join(base_dir, app_dir) for top, dirs, files in os.walk(app_dir): for fn in files: - if os.path.splitext(fn)[1] in ('.py', '.html'): + if os.path.splitext(fn)[1] in (".py", ".html"): fpath = os.path.join(top, fn) annotation_lines = [] - with open(fpath, 'r') as fd: + with open(fpath, "r") as fd: i = 0 for line in fd.readlines(): i += 1 if ANNOTATION_RE.search(line): tag, msg = ANNOTATION_RE.findall(line)[0] - if options['tag']: - if tag not in map(str.upper, map(str, options['tag'])): + if options["tag"]: + if tag not in map( + str.upper, map(str, options["tag"]) + ): break if ANNOTATION_END_RE.search(msg.strip()): - msg = ANNOTATION_END_RE.findall(msg.strip())[0][0] + msg = ANNOTATION_END_RE.findall(msg.strip())[0][ + 0 + ] - annotation_lines.append("[%3s] %-5s %s" % (i, tag, msg.strip())) + annotation_lines.append( + "[%3s] %-5s %s" % (i, tag, msg.strip()) + ) if annotation_lines: self.stdout.write("%s:" % fpath) for annotation in annotation_lines: diff --git a/django_extensions/management/commands/pipchecker.py b/django_extensions/management/commands/pipchecker.py deleted file mode 100644 index 5615d5099..000000000 --- a/django_extensions/management/commands/pipchecker.py +++ /dev/null @@ -1,326 +0,0 @@ -# -*- coding: utf-8 -*- -import json -import os -import re -from distutils.version import LooseVersion -from urllib.parse import urlparse -from urllib.error import HTTPError -from urllib.request import Request, urlopen -from xmlrpc.client import ServerProxy, Fault - -import pip -from time import sleep -from django.core.management.base import BaseCommand, CommandError -from django_extensions.management.color import color_style -from django_extensions.management.utils import signalcommand -from pip._internal.req import InstallRequirement - -if LooseVersion(pip.__version__) >= LooseVersion('19.0'): - from pip._internal.req.constructors import install_req_from_line # noqa - -try: - try: - from pip._internal.network.session import PipSession - except ImportError: - 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 # 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") - -try: - import requests - HAS_REQUESTS = True -except ImportError: - HAS_REQUESTS = False - - -class Command(BaseCommand): - help = "Scan pip requirement files for out-of-date packages." - - def add_arguments(self, parser): - super().add_arguments(parser) - parser.add_argument( - "-t", "--github-api-token", action="store", - dest="github_api_token", help="A github api authentication token." - ) - parser.add_argument( - "-r", "--requirement", action="append", dest="requirements", - default=[], metavar="FILENAME", - help="Check all the packages listed in the given requirements " - "file. This option can be used multiple times." - ), - parser.add_argument( - "-n", "--newer", action="store_true", dest="show_newer", - help="Also show when newer version then available is installed." - ) - - @signalcommand - def handle(self, *args, **options): - self.style = color_style() - - self.options = options - if options["requirements"]: - req_files = options["requirements"] - elif os.path.exists("requirements.txt"): - req_files = ["requirements.txt"] - elif os.path.exists("requirements"): - req_files = [ - "requirements/{0}".format(f) for f in os.listdir("requirements") - if os.path.isfile(os.path.join("requirements", f)) and f.lower().endswith(".txt") - ] - elif os.path.exists("requirements-dev.txt"): - req_files = ["requirements-dev.txt"] - elif os.path.exists("requirements-prod.txt"): - req_files = ["requirements-prod.txt"] - else: - raise CommandError("Requirements file(s) not found") - - self.reqs = {} - with PipSession() as session: - for filename in req_files: - for req in parse_requirements(filename, session=session): - if not isinstance(req, InstallRequirement): - req = install_req_from_line(req.requirement) - name = req.name if req.name else req.link.filename - - # url attribute changed to link in pip version 6.1.0 and above - if LooseVersion(pip.__version__) > LooseVersion('6.0.8'): - self.reqs[name] = { - "pip_req": req, - "url": req.link, - } - else: - self.reqs[name] = { - "pip_req": req, - "url": req.url, - } - - if options["github_api_token"]: - self.github_api_token = options["github_api_token"] - elif os.environ.get("GITHUB_API_TOKEN"): - self.github_api_token = os.environ.get("GITHUB_API_TOKEN") - else: - self.github_api_token = None # only 50 requests per hour - - self.check_pypi() - if HAS_REQUESTS: - self.check_github() - else: - self.stdout.write(self.style.ERROR("Cannot check github urls. The requests library is not installed. ( pip install requests )")) - self.check_other() - - def _urlopen_as_json(self, url, headers=None): - """Shorcut for return contents as json""" - req = Request(url, headers=headers) - return json.loads(urlopen(req).read()) - - def _is_stable(self, version): - return not re.search(r'([ab]|rc|dev)\d+$', str(version)) - - def _available_version(self, dist_version, available): - if self._is_stable(dist_version): - stable = [v for v in available if self._is_stable(LooseVersion(v))] - if stable: - return LooseVersion(stable[0]) - - return LooseVersion(available[0]) if available else None - - def check_pypi(self): - """If the requirement is frozen to pypi, check for a new version.""" - for dist in get_installed_distributions(): - name = dist.project_name - if name in self.reqs.keys(): - self.reqs[name]["dist"] = dist - - pypi = ServerProxy("https://pypi.python.org/pypi") - for name, req in list(self.reqs.items()): - if req["url"]: - continue # skipping github packages. - elif "dist" in req: - dist = req["dist"] - dist_version = LooseVersion(dist.version) - retry = True - available = None - while retry: - try: - available = pypi.package_releases(req["pip_req"].name, True) or pypi.package_releases(req["pip_req"].name.replace('-', '_'), True) - retry = False - except Fault as err: - self.stdout.write(err.faultString) - self.stdout.write("Retrying in 60 seconds!") - sleep(60) - - available_version = self._available_version(dist_version, available) - - if not available_version: - msg = self.style.WARN("release is not on pypi (check capitalization and/or --extra-index-url)") - elif self.options['show_newer'] and dist_version > available_version: - msg = self.style.INFO("{0} available (newer installed)".format(available_version)) - elif available_version > dist_version: - msg = self.style.INFO("{0} available".format(available_version)) - else: - msg = "up to date" - del self.reqs[name] - continue - pkg_info = self.style.BOLD("{dist.project_name} {dist.version}".format(dist=dist)) - else: - msg = "not installed" - pkg_info = name - self.stdout.write("{pkg_info:40} {msg}".format(pkg_info=pkg_info, msg=msg)) - del self.reqs[name] - - def check_github(self): - """ - If the requirement is frozen to a github url, check for new commits. - - API Tokens - ---------- - For more than 50 github api calls per hour, pipchecker requires - authentication with the github api by settings the environemnt - variable ``GITHUB_API_TOKEN`` or setting the command flag - --github-api-token='mytoken'``. - - To create a github api token for use at the command line:: - curl -u 'rizumu' -d '{"scopes":["repo"], "note":"pipchecker"}' https://api.github.com/authorizations - - For more info on github api tokens: - https://help.github.com/articles/creating-an-oauth-token-for-command-line-use - http://developer.github.com/v3/oauth/#oauth-authorizations-api - - Requirement Format - ------------------ - Pipchecker gets the sha of frozen repo and checks if it is - found at the head of any branches. If it is not found then - the requirement is considered to be out of date. - - Therefore, freezing at the commit hash will provide the expected - results, but if freezing at a branch or tag name, pipchecker will - not be able to determine with certainty if the repo is out of date. - - Freeze at the commit hash (sha):: - git+git://github.com/django/django.git@393c268e725f5b229ecb554f3fac02cfc250d2df#egg=Django - https://github.com/django/django/archive/393c268e725f5b229ecb554f3fac02cfc250d2df.tar.gz#egg=Django - https://github.com/django/django/archive/393c268e725f5b229ecb554f3fac02cfc250d2df.zip#egg=Django - - Freeze with a branch name:: - git+git://github.com/django/django.git@master#egg=Django - https://github.com/django/django/archive/master.tar.gz#egg=Django - https://github.com/django/django/archive/master.zip#egg=Django - - Freeze with a tag:: - git+git://github.com/django/django.git@1.5b2#egg=Django - https://github.com/django/django/archive/1.5b2.tar.gz#egg=Django - https://github.com/django/django/archive/1.5b2.zip#egg=Django - - Do not freeze:: - git+git://github.com/django/django.git#egg=Django - - """ - for name, req in list(self.reqs.items()): - req_url = req["url"] - if not req_url: - continue - req_url = str(req_url) - if req_url.startswith("git") and "github.com/" not in req_url: - continue - if req_url.endswith((".tar.gz", ".tar.bz2", ".zip")): - continue - - headers = { - "content-type": "application/json", - } - if self.github_api_token: - headers["Authorization"] = "token {0}".format(self.github_api_token) - try: - path_parts = urlparse(req_url).path.split("#", 1)[0].strip("/").rstrip("/").split("/") - - if len(path_parts) == 2: - user, repo = path_parts - - elif 'archive' in path_parts: - # Supports URL of format: - # https://github.com/django/django/archive/master.tar.gz#egg=Django - # https://github.com/django/django/archive/master.zip#egg=Django - user, repo = path_parts[:2] - repo += '@' + path_parts[-1].replace('.tar.gz', '').replace('.zip', '') - - else: - self.style.ERROR("\nFailed to parse %r\n" % (req_url, )) - continue - except (ValueError, IndexError) as e: - self.stdout.write(self.style.ERROR("\nFailed to parse %r: %s\n" % (req_url, e))) - continue - - try: - test_auth = requests.get("https://api.github.com/django/", headers=headers).json() - except HTTPError as e: - self.stdout.write("\n%s\n" % str(e)) - return - - if "message" in test_auth and test_auth["message"] == "Bad credentials": - self.stdout.write(self.style.ERROR("\nGithub API: Bad credentials. Aborting!\n")) - return - elif "message" in test_auth and test_auth["message"].startswith("API Rate Limit Exceeded"): - self.stdout.write(self.style.ERROR("\nGithub API: Rate Limit Exceeded. Aborting!\n")) - return - - frozen_commit_sha = None - if ".git" in repo: - repo_name, frozen_commit_full = repo.split(".git") - if frozen_commit_full.startswith("@"): - frozen_commit_sha = frozen_commit_full[1:] - elif "@" in repo: - repo_name, frozen_commit_sha = repo.split("@") - - if frozen_commit_sha is None: - msg = self.style.ERROR("repo is not frozen") - - if frozen_commit_sha: - branch_url = "https://api.github.com/repos/{0}/{1}/branches".format(user, repo_name) - branch_data = requests.get(branch_url, headers=headers).json() - - frozen_commit_url = "https://api.github.com/repos/{0}/{1}/commits/{2}".format( - user, repo_name, frozen_commit_sha - ) - frozen_commit_data = requests.get(frozen_commit_url, headers=headers).json() - - if "message" in frozen_commit_data and frozen_commit_data["message"] == "Not Found": - msg = self.style.ERROR("{0} not found in {1}. Repo may be private.".format(frozen_commit_sha[:10], name)) - elif frozen_commit_data["sha"] in [branch["commit"]["sha"] for branch in branch_data]: - msg = self.style.BOLD("up to date") - else: - msg = self.style.INFO("{0} is not the head of any branch".format(frozen_commit_data["sha"][:10])) - - if "dist" in req: - pkg_info = "{dist.project_name} {dist.version}".format(dist=req["dist"]) - elif frozen_commit_sha is None: - pkg_info = name - else: - pkg_info = "{0} {1}".format(name, frozen_commit_sha[:10]) - self.stdout.write("{pkg_info:40} {msg}".format(pkg_info=pkg_info, msg=msg)) - del self.reqs[name] - - def check_other(self): - """ - If the requirement is frozen somewhere other than pypi or github, skip. - - If you have a private pypi or use --extra-index-url, consider contributing - support here. - """ - if self.reqs: - self.stdout.write(self.style.ERROR("\nOnly pypi and github based requirements are supported:")) - for name, req in self.reqs.items(): - if "dist" in req: - pkg_info = "{dist.project_name} {dist.version}".format(dist=req["dist"]) - elif "url" in req: - pkg_info = "{url}".format(url=req["url"]) - else: - pkg_info = "unknown package" - self.stdout.write(self.style.BOLD("{pkg_info:40} is not a pypi or github requirement".format(pkg_info=pkg_info))) diff --git a/django_extensions/management/commands/print_settings.py b/django_extensions/management/commands/print_settings.py index 969bf0166..057d489d1 100644 --- a/django_extensions/management/commands/print_settings.py +++ b/django_extensions/management/commands/print_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ print_settings ============== @@ -21,63 +20,68 @@ class Command(BaseCommand): def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - 'setting', - nargs='*', - help='Specifies setting to be printed.' + "setting", nargs="*", help="Specifies setting to be printed." ) parser.add_argument( - '-f', '--fail', - action='store_true', - dest='fail', - help='Fail if invalid setting name is given.' + "-f", + "--fail", + action="store_true", + dest="fail", + help="Fail if invalid setting name is given.", ) parser.add_argument( - '--format', - default='simple', - dest='format', - help='Specifies output format.' + "--format", default="simple", dest="format", help="Specifies output format." ) parser.add_argument( - '--indent', + "--indent", default=4, - dest='indent', + dest="indent", type=int, - help='Specifies indent level for JSON and YAML' + help="Specifies indent level for JSON and YAML", ) @signalcommand def handle(self, *args, **options): - setting_names = options['setting'] + setting_names = options["setting"] settings_dct = {k: getattr(settings, k) for k in dir(settings) if k.isupper()} if setting_names: settings_dct = { - key: value for key, value in settings_dct.items() - if any(fnmatch.fnmatchcase(key, setting_name) for setting_name in setting_names) + key: value + for key, value in settings_dct.items() + if any( + fnmatch.fnmatchcase(key, setting_name) + for setting_name in setting_names + ) } - if options['fail']: + if options["fail"]: for setting_name in setting_names: - if not any(fnmatch.fnmatchcase(key, setting_name) for key in settings_dct.keys()): - raise CommandError('%s not found in settings.' % setting_name) + if not any( + fnmatch.fnmatchcase(key, setting_name) + for key in settings_dct.keys() + ): + raise CommandError("%s not found in settings." % setting_name) - output_format = options['format'] - indent = options['indent'] + output_format = options["format"] + indent = options["indent"] - if output_format == 'json': + if output_format == "json": print(json.dumps(settings_dct, indent=indent)) - elif output_format == 'yaml': + elif output_format == "yaml": import yaml # requires PyYAML + print(yaml.dump(settings_dct, indent=indent)) - elif output_format == 'pprint': + elif output_format == "pprint": from pprint import pprint + pprint(settings_dct) - elif output_format == 'text': + elif output_format == "text": for key, value in settings_dct.items(): print("%s = %s" % (key, value)) - elif output_format == 'value': + elif output_format == "value": for value in settings_dct.values(): print(value) else: for key, value in settings_dct.items(): - print('%-40s = %r' % (key, value)) + print("%-40s = %r" % (key, value)) diff --git a/django_extensions/management/commands/print_user_for_session.py b/django_extensions/management/commands/print_user_for_session.py index e12d839a0..5e36b00cb 100644 --- a/django_extensions/management/commands/print_user_for_session.py +++ b/django_extensions/management/commands/print_user_for_session.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import importlib from django.conf import settings @@ -9,18 +8,18 @@ class Command(BaseCommand): - help = ("print the user information for the provided session key. " - "this is very helpful when trying to track down the person who " - "experienced a site crash.") + help = ( + "print the user information for the provided session key. " + "this is very helpful when trying to track down the person who " + "experienced a site crash." + ) def add_arguments(self, parser): - parser.add_argument('session_id', nargs='+', type=str, - help='user session id') + parser.add_argument("session_id", nargs="+", type=str, help="user session id") @signalcommand def handle(self, *args, **options): - - key = options['session_id'][0] + key = options["session_id"][0] if not set(key).issubset(set(VALID_KEY_CHARS)): raise CommandError("malformed session key") @@ -34,20 +33,20 @@ def handle(self, *args, **options): session = engine.SessionStore(key) data = session.load() - print('Session to Expire: %s' % session.get_expiry_date()) - print('Raw Data: %s' % data) + print("Session to Expire: %s" % session.get_expiry_date()) + print("Raw Data: %s" % data) uid = data.get(SESSION_KEY, None) backend_path = data.get(BACKEND_SESSION_KEY, None) if backend_path is None: - print('No authentication backend associated with session') + print("No authentication backend associated with session") return if uid is None: - print('No user associated with session') + print("No user associated with session") return - print(u"User id: %s" % uid) + print("User id: %s" % uid) backend = load_backend(backend_path) user = backend.get_user(user_id=uid) @@ -59,5 +58,5 @@ def handle(self, *args, **options): print("full name: %s" % user.get_full_name()) print("short name: %s" % user.get_short_name()) print("username: %s" % user.get_username()) - if hasattr(user, 'email'): + if hasattr(user, "email"): print("email: %s" % user.email) diff --git a/django_extensions/management/commands/raise_test_exception.py b/django_extensions/management/commands/raise_test_exception.py new file mode 100644 index 000000000..4f69c2f54 --- /dev/null +++ b/django_extensions/management/commands/raise_test_exception.py @@ -0,0 +1,22 @@ +from django.core.management.base import BaseCommand + +from django_extensions.management.utils import signalcommand + + +class DjangoExtensionsTestException(Exception): + pass + + +class Command(BaseCommand): + help = ( + "Raises a test Exception named DjangoExtensionsTestException. " + "Useful for debugging integration with error reporters like Sentry." + ) + + @signalcommand + def handle(self, *args, **options): + message = ( + "This is a test exception via the " + "django-extensions raise_test_exception management command." + ) + raise DjangoExtensionsTestException(message) diff --git a/django_extensions/management/commands/reset_db.py b/django_extensions/management/commands/reset_db.py index bca8247f4..d91cbb073 100644 --- a/django_extensions/management/commands/reset_db.py +++ b/django_extensions/management/commands/reset_db.py @@ -1,9 +1,10 @@ -# -*- coding: utf-8 -*- """ reset_db command -originally from http://www.djangosnippets.org/snippets/828/ by dnordberg +originally from https://www.djangosnippets.org/snippets/828/ by dnordberg """ + +import importlib.util import os import logging import warnings @@ -24,42 +25,75 @@ class Command(BaseCommand): def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--noinput', action='store_false', - dest='interactive', default=True, - help='Tells Django to NOT prompt the user for input of any kind.' + "--noinput", + "--no-input", + action="store_false", + dest="interactive", + default=True, + help="Tells Django to NOT prompt the user for input of any kind.", ) parser.add_argument( - '--no-utf8', action='store_true', dest='no_utf8_support', + "--no-utf8", + action="store_true", + dest="no_utf8_support", default=False, - help='Tells Django to not create a UTF-8 charset database' + help="Tells Django to not create a UTF-8 charset database", ) parser.add_argument( - '-U', '--user', action='store', dest='user', default=None, - help='Use another user for the database than defined in settings.py' + "-U", + "--user", + action="store", + dest="user", + default=None, + help="Use another user for the database than defined in settings.py", ) parser.add_argument( - '-O', '--owner', action='store', dest='owner', default=None, - help='Use another owner for creating the database than the user defined in settings or via --user' + "-O", + "--owner", + action="store", + dest="owner", + default=None, + help="Use another owner for creating the database than the user defined " + "in settings or via --user", ) parser.add_argument( - '-P', '--password', action='store', dest='password', default=None, - help='Use another password for the database than defined in settings.py' + "-P", + "--password", + action="store", + dest="password", + default=None, + help="Use another password for the database than defined in settings.py", ) parser.add_argument( - '-D', '--dbname', action='store', dest='dbname', default=None, - help='Use another database name than defined in settings.py' + "-D", + "--dbname", + action="store", + dest="dbname", + default=None, + help="Use another database name than defined in settings.py", ) parser.add_argument( - '-R', '--router', action='store', dest='router', default=DEFAULT_DB_ALIAS, - help='Use this router-database other than defined in settings.py' + "-R", + "--router", + action="store", + dest="router", + default=DEFAULT_DB_ALIAS, + help="Use this router-database other than defined in settings.py", ) parser.add_argument( - '--database', default=DEFAULT_DB_ALIAS, - help='Nominates a database to run command for. Defaults to the "%s" database.' % DEFAULT_DB_ALIAS, + "--database", + default=DEFAULT_DB_ALIAS, + help='Nominates a database to run command for. Defaults to the "%s".' + % DEFAULT_DB_ALIAS, ) parser.add_argument( - '-c', '--close-sessions', action='store_true', dest='close_sessions', default=False, - help='Close database connections before dropping database (PostgreSQL only)' + "-c", + "--close-sessions", + action="store_true", + dest="close_sessions", + default=False, + help="Close database connections before dropping database " + "(currently works on PostgreSQL only)", ) @signalcommand @@ -70,45 +104,56 @@ def handle(self, *args, **options): Note: Transaction wrappers are in reverse as a work around for autocommit, anybody know how to do this the right way? """ - database = options['database'] - if options['router'] != DEFAULT_DB_ALIAS: - warnings.warn("--router is deprecated. You should use --database.", RemovedInNextVersionWarning, stacklevel=2) - database = options['router'] + database = options["database"] + if options["router"] != DEFAULT_DB_ALIAS: + warnings.warn( + "--router is deprecated. You should use --database.", + RemovedInNextVersionWarning, + stacklevel=2, + ) + database = options["router"] dbinfo = settings.DATABASES.get(database) if dbinfo is None: raise CommandError("Unknown database %s" % database) - engine = dbinfo.get('ENGINE') + engine = dbinfo.get("ENGINE") - user = password = database_name = database_host = database_port = '' - if engine == 'mysql': - (user, password, database_name, database_host, database_port) = parse_mysql_cnf(dbinfo) + user = password = database_name = database_host = database_port = "" + if engine == "mysql": + (user, password, database_name, database_host, database_port) = ( + parse_mysql_cnf(dbinfo) + ) - user = options['user'] or dbinfo.get('USER') or user - password = options['password'] or dbinfo.get('PASSWORD') or password - owner = options['owner'] or user + user = options["user"] or dbinfo.get("USER") or user + password = options["password"] or dbinfo.get("PASSWORD") or password + owner = options["owner"] or user - database_name = options['dbname'] or dbinfo.get('NAME') or database_name - if database_name == '': - raise CommandError("You need to specify DATABASE_NAME in your Django settings file.") + database_name = options["dbname"] or dbinfo.get("NAME") or database_name + if database_name == "": + raise CommandError( + "You need to specify DATABASE_NAME in your Django settings file." + ) - database_host = dbinfo.get('HOST') or database_host - database_port = dbinfo.get('PORT') or database_port + database_host = dbinfo.get("HOST") or database_host + database_port = dbinfo.get("PORT") or database_port verbosity = options["verbosity"] - if options['interactive']: - confirm = input(""" + if options["interactive"]: + confirm = input( + """ You have requested a database reset. This will IRREVERSIBLY DESTROY ALL data in the database "%s". Are you sure you want to do this? -Type 'yes' to continue, or 'no' to cancel: """ % (database_name,)) +Type 'yes' to continue, or 'no' to cancel: """ + % (database_name,) + ) else: - confirm = 'yes' + confirm = "yes" - if confirm != 'yes': + if confirm != "yes": print("Reset cancelled.") return @@ -120,76 +165,87 @@ def handle(self, *args, **options): pass elif engine in MYSQL_ENGINES: import MySQLdb as Database + kwargs = { - 'user': user, - 'passwd': password, + "user": user, + "passwd": password, } - if database_host.startswith('/'): - kwargs['unix_socket'] = database_host + if database_host.startswith("/"): + kwargs["unix_socket"] = database_host else: - kwargs['host'] = database_host + kwargs["host"] = database_host if database_port: - kwargs['port'] = int(database_port) + kwargs["port"] = int(database_port) connection = Database.connect(**kwargs) - drop_query = 'DROP DATABASE IF EXISTS `%s`' % database_name - utf8_support = '' if options['no_utf8_support'] else 'CHARACTER SET utf8' - create_query = 'CREATE DATABASE `%s` %s' % (database_name, utf8_support) + drop_query = "DROP DATABASE IF EXISTS `%s`" % database_name + utf8_support = "" if options["no_utf8_support"] else "CHARACTER SET utf8" + create_query = "CREATE DATABASE `%s` %s" % (database_name, utf8_support) logging.info('Executing... "%s"', drop_query) connection.query(drop_query) logging.info('Executing... "%s"', create_query) connection.query(create_query.strip()) elif engine in POSTGRESQL_ENGINES: - import psycopg2 as Database # NOQA + has_psycopg3 = importlib.util.find_spec("psycopg") + if has_psycopg3: + import psycopg as Database # NOQA + else: + import psycopg2 as Database # NOQA - conn_params = {'database': 'template1'} + conn_params = {"dbname": "template1"} if user: - conn_params['user'] = user + conn_params["user"] = user if password: - conn_params['password'] = password + conn_params["password"] = password if database_host: - conn_params['host'] = database_host + conn_params["host"] = database_host if database_port: - conn_params['port'] = database_port + conn_params["port"] = database_port connection = Database.connect(**conn_params) - connection.set_isolation_level(0) # autocommit false + if has_psycopg3: + connection.autocommit = True + else: + connection.set_isolation_level(0) # autocommit false cursor = connection.cursor() - if options['close_sessions']: - close_sessions_query = """ + if options["close_sessions"]: + close_sessions_query = ( + """ SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '%s'; - """ % database_name + """ + % database_name + ) logging.info('Executing... "%s"', close_sessions_query.strip()) try: cursor.execute(close_sessions_query) except Database.ProgrammingError as e: logging.exception("Error: %s", str(e)) - drop_query = "DROP DATABASE \"%s\";" % database_name + drop_query = 'DROP DATABASE "%s";' % database_name logging.info('Executing... "%s"', drop_query) try: cursor.execute(drop_query) except Database.ProgrammingError as e: logging.exception("Error: %s", str(e)) - create_query = "CREATE DATABASE \"%s\"" % database_name + create_query = 'CREATE DATABASE "%s"' % database_name if owner: - create_query += " WITH OWNER = \"%s\" " % owner + create_query += ' WITH OWNER = "%s" ' % owner create_query += " ENCODING = 'UTF8'" if settings.DEFAULT_TABLESPACE: - create_query += ' TABLESPACE = %s;' % settings.DEFAULT_TABLESPACE + create_query += " TABLESPACE = %s;" % settings.DEFAULT_TABLESPACE else: - create_query += ';' + create_query += ";" logging.info('Executing... "%s"', create_query) cursor.execute(create_query) else: raise CommandError("Unknown database engine %s" % engine) - if verbosity >= 2 or options['interactive']: + if verbosity >= 2 or options["interactive"]: print("Reset successful.") diff --git a/django_extensions/management/commands/reset_schema.py b/django_extensions/management/commands/reset_schema.py index 95b567325..e456b8a50 100644 --- a/django_extensions/management/commands/reset_schema.py +++ b/django_extensions/management/commands/reset_schema.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Recreates the public schema for current database (PostgreSQL only). Useful for Docker environments where you need to reset database @@ -24,53 +23,74 @@ class Command(BaseCommand): def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--noinput', action='store_false', - dest='interactive', default=True, - help='Tells Django to NOT prompt the user for input of any kind.' + "--noinput", + "--no-input", + action="store_false", + dest="interactive", + default=True, + help="Tells Django to NOT prompt the user for input of any kind.", ) parser.add_argument( - '-R', '--router', action='store', dest='router', default=DEFAULT_DB_ALIAS, - help='Use this router-database instead of the one defined in settings.py' + "-R", + "--router", + action="store", + dest="router", + default=DEFAULT_DB_ALIAS, + help="Use this router-database instead of the one defined in settings.py", ) parser.add_argument( - '--database', default=DEFAULT_DB_ALIAS, - help='Nominates a database to run command for. Defaults to the "%s" database.' % DEFAULT_DB_ALIAS, + "--database", + default=DEFAULT_DB_ALIAS, + help='Nominates a database to run command for. Defaults to the "%s".' + % DEFAULT_DB_ALIAS, ) parser.add_argument( - '-S', '--schema', action='store', dest='schema', default='public', - help='Drop this schema instead of "public"' + "-S", + "--schema", + action="store", + dest="schema", + default="public", + help='Drop this schema instead of "public"', ) def handle(self, *args, **options): - database = options['database'] - if options['router'] != DEFAULT_DB_ALIAS: - warnings.warn("--router is deprecated. You should use --database.", RemovedInNextVersionWarning, stacklevel=2) - database = options['router'] + database = options["database"] + if options["router"] != DEFAULT_DB_ALIAS: + warnings.warn( + "--router is deprecated. You should use --database.", + RemovedInNextVersionWarning, + stacklevel=2, + ) + database = options["router"] dbinfo = settings.DATABASES.get(database) if dbinfo is None: raise CommandError("Unknown database %s" % database) - engine = dbinfo.get('ENGINE') + engine = dbinfo.get("ENGINE") if engine not in POSTGRESQL_ENGINES: - raise CommandError('This command can be used only with PostgreSQL databases.') + raise CommandError( + "This command can be used only with PostgreSQL databases." + ) - database_name = dbinfo['NAME'] + database_name = dbinfo["NAME"] - schema = options['schema'] + schema = options["schema"] - if options['interactive']: - confirm = input(""" + if options["interactive"]: + confirm = input( + """ You have requested a database schema reset. This will IRREVERSIBLY DESTROY ALL data in the "{}" schema of database "{}". Are you sure you want to do this? -Type 'yes' to continue, or 'no' to cancel: """.format(schema, database_name)) +Type 'yes' to continue, or 'no' to cancel: """.format(schema, database_name) + ) else: - confirm = 'yes' + confirm = "yes" - if confirm != 'yes': + if confirm != "yes": print("Reset cancelled.") return diff --git a/django_extensions/management/commands/runjob.py b/django_extensions/management/commands/runjob.py index cf1163ec4..2ecd31c4d 100644 --- a/django_extensions/management/commands/runjob.py +++ b/django_extensions/management/commands/runjob.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import logging from django.core.management.base import BaseCommand @@ -15,11 +14,15 @@ class Command(BaseCommand): def add_arguments(self, parser): super().add_arguments(parser) - parser.add_argument('app_name', nargs='?') - parser.add_argument('job_name', nargs='?') + parser.add_argument("app_name", nargs="?") + parser.add_argument("job_name", nargs="?") parser.add_argument( - '--list', '-l', action="store_true", dest="list_jobs", - default=False, help="List all jobs with their description" + "--list", + "-l", + action="store_true", + dest="list_jobs", + default=False, + help="List all jobs with their description", ) def runjob(self, app_name, job_name, options): @@ -30,7 +33,9 @@ def runjob(self, app_name, job_name, options): job = get_job(app_name, job_name) except KeyError: if app_name: - logger.error("Error: Job %s for applabel %s not found", job_name, app_name) + logger.error( + "Error: Job %s for applabel %s not found", job_name, app_name + ) else: logger.error("Error: Job %s not found", job_name) logger.info("Use -l option to view all the available jobs") @@ -42,8 +47,8 @@ def runjob(self, app_name, job_name, options): @signalcommand def handle(self, *args, **options): - app_name = options['app_name'] - job_name = options['job_name'] + app_name = options["app_name"] + job_name = options["job_name"] # hack since we are using job_name nargs='?' for -l to work if app_name and not job_name: @@ -52,7 +57,7 @@ def handle(self, *args, **options): setup_logger(logger, self.stdout) - if options['list_jobs']: + if options["list_jobs"]: print_jobs(only_scheduled=False, show_when=True, show_appname=True) else: self.runjob(app_name, job_name, options) diff --git a/django_extensions/management/commands/runjobs.py b/django_extensions/management/commands/runjobs.py index ecddc075b..43844886f 100644 --- a/django_extensions/management/commands/runjobs.py +++ b/django_extensions/management/commands/runjobs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import logging from django.apps import apps @@ -13,21 +12,32 @@ class Command(BaseCommand): help = "Runs scheduled maintenance jobs." - when_options = ['minutely', 'quarter_hourly', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'] + when_options = [ + "minutely", + "quarter_hourly", + "hourly", + "daily", + "weekly", + "monthly", + "yearly", + ] def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - 'when', nargs='?', - help="options: %s" % ', '.join(self.when_options) + "when", nargs="?", help="options: %s" % ", ".join(self.when_options) ) parser.add_argument( - '--list', '-l', action="store_true", dest="list_jobs", - default=False, help="List all jobs with their description" + "--list", + "-l", + action="store_true", + dest="list_jobs", + default=False, + help="List all jobs with their description", ) def usage_msg(self): - print("%s Please specify: %s" % (self.help, ', '.join(self.when_options))) + print("%s Please specify: %s" % (self.help, ", ".join(self.when_options))) def runjobs(self, when, options): verbosity = options["verbosity"] @@ -39,10 +49,12 @@ def runjobs(self, when, options): try: job().execute() except Exception: - logger.exception("ERROR OCCURED IN JOB: %s (APP: %s)", job_name, app_name) + logger.exception( + "ERROR OCCURED IN JOB: %s (APP: %s)", job_name, app_name + ) def runjobs_by_signals(self, when, options): - """ Run jobs from the signals """ + """Run jobs from the signals""" # Thanks for Ian Holsman for the idea and code from django_extensions.management import signals from django.conf import settings @@ -50,36 +62,38 @@ def runjobs_by_signals(self, when, options): verbosity = options["verbosity"] for app_name in settings.INSTALLED_APPS: try: - __import__(app_name + '.management', '', '', ['']) + __import__(app_name + ".management", "", "", [""]) except ImportError: pass - for app in (app.models_module for app in apps.get_app_configs() if app.models_module): + for app in ( + app.models_module for app in apps.get_app_configs() if app.models_module + ): if verbosity > 1: - app_name = '.'.join(app.__name__.rsplit('.')[:-1]) + app_name = ".".join(app.__name__.rsplit(".")[:-1]) print("Sending %s job signal for: %s" % (when, app_name)) - if when == 'minutely': + if when == "minutely": signals.run_minutely_jobs.send(sender=app, app=app) - elif when == 'quarter_hourly': + elif when == "quarter_hourly": signals.run_quarter_hourly_jobs.send(sender=app, app=app) - elif when == 'hourly': + elif when == "hourly": signals.run_hourly_jobs.send(sender=app, app=app) - elif when == 'daily': + elif when == "daily": signals.run_daily_jobs.send(sender=app, app=app) - elif when == 'weekly': + elif when == "weekly": signals.run_weekly_jobs.send(sender=app, app=app) - elif when == 'monthly': + elif when == "monthly": signals.run_monthly_jobs.send(sender=app, app=app) - elif when == 'yearly': + elif when == "yearly": signals.run_yearly_jobs.send(sender=app, app=app) @signalcommand def handle(self, *args, **options): - when = options['when'] + when = options["when"] setup_logger(logger, self.stdout) - if options['list_jobs']: + if options["list_jobs"]: print_jobs(when, only_scheduled=True, show_when=True, show_appname=True) elif when in self.when_options: self.runjobs(when, options) diff --git a/django_extensions/management/commands/runprofileserver.py b/django_extensions/management/commands/runprofileserver.py index 9859fb648..c843d8e7a 100644 --- a/django_extensions/management/commands/runprofileserver.py +++ b/django_extensions/management/commands/runprofileserver.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ runprofileserver.py @@ -20,7 +19,7 @@ from django_extensions.management.utils import signalcommand -USE_STATICFILES = 'django.contrib.staticfiles' in settings.INSTALLED_APPS +USE_STATICFILES = "django.contrib.staticfiles" in settings.INSTALLED_APPS class KCacheGrind: @@ -30,7 +29,7 @@ def __init__(self, profiler): def output(self, out_file): self.out_file = out_file - self.out_file.write('events: Ticks\n') + self.out_file.write("events: Ticks\n") self._print_summary() for entry in self.data: self._entry(entry) @@ -40,23 +39,23 @@ def _print_summary(self): for entry in self.data: totaltime = int(entry.totaltime * 1000) max_cost = max(max_cost, totaltime) - self.out_file.write('summary: %d\n' % (max_cost,)) + self.out_file.write("summary: %d\n" % (max_cost,)) def _entry(self, entry): out_file = self.out_file code = entry.code if isinstance(code, str): - out_file.write('fn=%s\n' % code) + out_file.write("fn=%s\n" % code) else: - out_file.write('fl=%s\n' % code.co_filename) - out_file.write('fn=%s\n' % code.co_name) + out_file.write("fl=%s\n" % code.co_filename) + out_file.write("fn=%s\n" % code.co_name) inlinetime = int(entry.inlinetime * 1000) if isinstance(code, str): - out_file.write('0 %s\n' % inlinetime) + out_file.write("0 %s\n" % inlinetime) else: - out_file.write('%d %d\n' % (code.co_firstlineno, inlinetime)) + out_file.write("%d %d\n" % (code.co_firstlineno, inlinetime)) # recursive calls are counted in entry.calls if entry.calls: @@ -77,156 +76,153 @@ def _subentry(self, lineno, subentry): out_file = self.out_file code = subentry.code if isinstance(code, str): - out_file.write('cfn=%s\n' % code) - out_file.write('calls=%d 0\n' % (subentry.callcount,)) + out_file.write("cfn=%s\n" % code) + out_file.write("calls=%d 0\n" % (subentry.callcount,)) else: - out_file.write('cfl=%s\n' % code.co_filename) - out_file.write('cfn=%s\n' % code.co_name) - out_file.write('calls=%d %d\n' % (subentry.callcount, code.co_firstlineno)) + out_file.write("cfl=%s\n" % code.co_filename) + out_file.write("cfn=%s\n" % code.co_name) + out_file.write("calls=%d %d\n" % (subentry.callcount, code.co_firstlineno)) totaltime = int(subentry.totaltime * 1000) - out_file.write('%d %d\n' % (lineno, totaltime)) + out_file.write("%d %d\n" % (lineno, totaltime)) class Command(BaseCommand): help = "Starts a lightweight Web server with profiling enabled." - args = '[optional port number, or ipaddr:port]' + args = "[optional port number, or ipaddr:port]" def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - 'addrport', nargs='?', - help='Optional port number, or ipaddr:port' + "addrport", nargs="?", help="Optional port number, or ipaddr:port" ) parser.add_argument( - '--noreload', action='store_false', dest='use_reloader', + "--noreload", + action="store_false", + dest="use_reloader", default=True, - help='Tells Django to NOT use the auto-reloader.') - parser.add_argument( - '--nothreading', action='store_false', dest='use_threading', default=True, - help='Tells Django to NOT use threading.', + help="Tells Django to NOT use the auto-reloader.", ) parser.add_argument( - '--prof-path', dest='prof_path', default='/tmp', - help='Specifies the directory which to save profile information ' - 'in.' + "--nothreading", + action="store_false", + dest="use_threading", + default=True, + help="Tells Django to NOT use threading.", ) parser.add_argument( - '--prof-file', dest='prof_file', - default='{path}.{duration:06d}ms.{time}', - help='Set filename format, default if ' - '"{path}.{duration:06d}ms.{time}".' + "--prof-path", + dest="prof_path", + default="/tmp", + help="Specifies the directory which to save profile information in.", ) parser.add_argument( - '--nomedia', action='store_true', dest='no_media', default=False, - help='Do not profile MEDIA_URL' + "--prof-file", + dest="prof_file", + default="{path}.{duration:06d}ms.{time}", + help='Set filename format, default if "{path}.{duration:06d}ms.{time}".', ) parser.add_argument( - '--use-cprofile', action='store_true', dest='use_cprofile', + "--nomedia", + action="store_true", + dest="no_media", default=False, - help='Use cProfile if available, this is disabled per default ' - 'because of incompatibilities.' + help="Do not profile MEDIA_URL", ) parser.add_argument( - '--kcachegrind', action='store_true', dest='use_lsprof', + "--kcachegrind", + action="store_true", + dest="use_lsprof", default=False, - help='Create kcachegrind compatible lsprof files, this requires ' - 'and automatically enables cProfile.' + help="Create kcachegrind compatible lsprof files, this requires " + "and automatically enables cProfile.", ) if USE_STATICFILES: parser.add_argument( - '--nostatic', action="store_false", dest='use_static_handler', + "--nostatic", + action="store_false", + dest="use_static_handler", default=True, - help='Tells Django to NOT automatically serve static files ' - 'at STATIC_URL.') + help="Tells Django to NOT automatically serve static files " + "at STATIC_URL.", + ) parser.add_argument( - '--insecure', action="store_true", dest='insecure_serving', + "--insecure", + action="store_true", + dest="insecure_serving", default=False, - help='Allows serving static files even if DEBUG is False.') + help="Allows serving static files even if DEBUG is False.", + ) @signalcommand - def handle(self, addrport='', *args, **options): + def handle(self, addrport="", *args, **options): import django import socket import errno from django.core.servers.basehttp import run if not addrport: - addr = '' - port = '8000' + addr = "" + port = "8000" else: try: - addr, port = addrport.split(':') + addr, port = addrport.split(":") except ValueError: - addr, port = '', addrport + addr, port = "", addrport if not addr: - addr = '127.0.0.1' + addr = "127.0.0.1" if not port.isdigit(): raise CommandError("%r is not a valid port number." % port) - use_reloader = options['use_reloader'] - shutdown_message = options.get('shutdown_message', '') - no_media = options['no_media'] - quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C' + use_reloader = options["use_reloader"] + shutdown_message = options.get("shutdown_message", "") + no_media = options["no_media"] + quit_command = (sys.platform == "win32") and "CTRL-BREAK" or "CONTROL-C" def inner_run(): import os import time - try: - import hotshot - HAS_HOTSHOT = True - except ImportError: - HAS_HOTSHOT = False # python 3.x - USE_CPROFILE = options['use_cprofile'] - USE_LSPROF = options['use_lsprof'] - if USE_LSPROF: - USE_CPROFILE = True - if USE_CPROFILE: - try: - import cProfile - USE_CPROFILE = True - except ImportError: - print("cProfile disabled, module cannot be imported!") - USE_CPROFILE = False - if USE_LSPROF and not USE_CPROFILE: - raise CommandError("Kcachegrind compatible output format required cProfile from Python 2.5") - if not HAS_HOTSHOT and not USE_CPROFILE: - raise CommandError("Hotshot profile library not found. (and not using cProfile)") + import cProfile - prof_path = options['prof_path'] + USE_LSPROF = options["use_lsprof"] - prof_file = options['prof_file'] - if not prof_file.format(path='1', duration=2, time=3): - prof_file = '{path}.{duration:06d}ms.{time}' - print("Filename format is wrong. Default format used: '{path}.{duration:06d}ms.{time}'.") + prof_path = options["prof_path"] + + prof_file = options["prof_file"] + if not prof_file.format(path="1", duration=2, time=3): + prof_file = "{path}.{duration:06d}ms.{time}" + print( + "Filename format is wrong. " + "Default format used: '{path}.{duration:06d}ms.{time}'." + ) def get_exclude_paths(): exclude_paths = [] - media_url = getattr(settings, 'MEDIA_URL', None) + media_url = getattr(settings, "MEDIA_URL", None) if media_url: exclude_paths.append(media_url) - static_url = getattr(settings, 'STATIC_URL', None) + static_url = getattr(settings, "STATIC_URL", None) if static_url: exclude_paths.append(static_url) return exclude_paths def make_profiler_handler(inner_handler): def handler(environ, start_response): - path_info = environ['PATH_INFO'] - # when using something like a dynamic site middleware is could be necessary - # to refetch the exclude_paths every time since they could change per site. - if no_media and any(path_info.startswith(p) for p in get_exclude_paths()): + path_info = environ["PATH_INFO"] + # when using something like a dynamic site middleware is could be + # necessary to refetch the exclude_paths every time since they could + # change per site. + if no_media and any( + path_info.startswith(p) for p in get_exclude_paths() + ): return inner_handler(environ, start_response) - path_name = path_info.strip("/").replace('/', '.') or "root" + path_name = path_info.strip("/").replace("/", ".") or "root" profname = "%s.%d.prof" % (path_name, time.time()) profname = os.path.join(prof_path, profname) - if USE_CPROFILE: - prof = cProfile.Profile() - else: - prof = hotshot.Profile(profname) + prof = cProfile.Profile() start = datetime.now() try: return prof.runcall(inner_handler, environ, start_response) @@ -236,32 +232,36 @@ def handler(environ, start_response): elapms = elap.seconds * 1000.0 + elap.microseconds / 1000.0 if USE_LSPROF: kg = KCacheGrind(prof) - with open(profname, 'w') as f: + with open(profname, "w") as f: kg.output(f) - elif USE_CPROFILE: + else: prof.dump_stats(profname) - profname2 = prof_file.format(path=path_name, duration=int(elapms), time=int(time.time())) + profname2 = prof_file.format( + path=path_name, duration=int(elapms), time=int(time.time()) + ) profname2 = os.path.join(prof_path, "%s.prof" % profname2) - if not USE_CPROFILE: - prof.close() os.rename(profname, profname2) + return handler print("Performing system checks...") self.check(display_num_errors=True) - print("\nDjango version %s, using settings %r" % (django.get_version(), settings.SETTINGS_MODULE)) + print( + "\nDjango version %s, using settings %r" + % (django.get_version(), settings.SETTINGS_MODULE) + ) print("Development server is running at http://%s:%s/" % (addr, port)) print("Quit the server with %s." % quit_command) try: handler = get_internal_wsgi_application() if USE_STATICFILES: - use_static_handler = options['use_static_handler'] - insecure_serving = options['insecure_serving'] + use_static_handler = options["use_static_handler"] + insecure_serving = options["insecure_serving"] if use_static_handler and (settings.DEBUG or insecure_serving): handler = StaticFilesHandler(handler) handler = make_profiler_handler(handler) - run(addr, int(port), handler, threading=options['use_threading']) + run(addr, int(port), handler, threading=options["use_threading"]) except socket.error as e: # Use helpful error messages instead of ugly tracebacks. ERRORS = { @@ -273,19 +273,22 @@ def handler(environ, start_response): error_text = ERRORS[e.errno] except (AttributeError, KeyError): error_text = str(e) - sys.stderr.write(self.style.ERROR("Error: %s" % error_text) + '\n') + sys.stderr.write(self.style.ERROR("Error: %s" % error_text) + "\n") # Need to use an OS exit because sys.exit doesn't work in a thread os._exit(1) except KeyboardInterrupt: if shutdown_message: print(shutdown_message) sys.exit(0) + if use_reloader: try: from django.utils.autoreload import run_with_reloader + run_with_reloader(inner_run) except ImportError: from django.utils import autoreload + autoreload.main(inner_run) else: inner_run() diff --git a/django_extensions/management/commands/runscript.py b/django_extensions/management/commands/runscript.py index a6739029d..5e64e8873 100644 --- a/django_extensions/management/commands/runscript.py +++ b/django_extensions/management/commands/runscript.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import sys import importlib @@ -16,9 +15,9 @@ class DirPolicyChoices: - NONE = 'none' - EACH = 'each' - ROOT = 'root' + NONE = "none" + EACH = "each" + ROOT = "root" def check_is_directory(value): @@ -29,15 +28,17 @@ def check_is_directory(value): class BadCustomDirectoryException(Exception): def __init__(self, value): - self.message = value + ' If --dir-policy is custom than you must set correct directory in ' \ - '--dir option or in settings.RUNSCRIPT_CHDIR' + self.message = ( + value + " If --dir-policy is custom than you must set correct directory in " + "--dir option or in settings.RUNSCRIPT_CHDIR" + ) def __str__(self): return self.message class Command(EmailNotificationCommand): - help = 'Runs a script in django context.' + help = "Runs a script in django context." def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -46,77 +47,101 @@ def __init__(self, *args, **kwargs): def add_arguments(self, parser): super().add_arguments(parser) - parser.add_argument('script', nargs='+') + parser.add_argument("script", nargs="+") parser.add_argument( - '--fixtures', action='store_true', dest='infixtures', default=False, - help='Also look in app.fixtures subdir', + "--fixtures", + action="store_true", + dest="infixtures", + default=False, + help="Also look in app.fixtures subdir", ) parser.add_argument( - '--noscripts', action='store_true', dest='noscripts', default=False, - help='Do not look in app.scripts subdir', + "--noscripts", + action="store_true", + dest="noscripts", + default=False, + help="Do not look in app.scripts subdir", ) parser.add_argument( - '-s', '--silent', action='store_true', dest='silent', default=False, - help='Run silently, do not show errors and tracebacks. Also implies --continue-on-error.', + "-s", + "--silent", + action="store_true", + dest="silent", + default=False, + help="Run silently, do not show errors and tracebacks." + " Also implies --continue-on-error.", ) parser.add_argument( - '-c', '--continue-on-error', action='store_true', dest='continue_on_error', default=False, - help='Continue executing other scripts even though one has failed. ' - 'It will print a traceback unless --no-traceback or --silent are given ' - 'The exit code used when terminating will always be 1.', + "-c", + "--continue-on-error", + action="store_true", + dest="continue_on_error", + default=False, + help="Continue executing other scripts even though one has failed. " + "It will print a traceback unless --no-traceback or --silent are given " + "The exit code used when terminating will always be 1.", ) parser.add_argument( - '--no-traceback', action='store_true', dest='no_traceback', default=False, - help='Do not show tracebacks', + "--no-traceback", + action="store_true", + dest="no_traceback", + default=False, + help="Do not show tracebacks", ) parser.add_argument( - '--script-args', nargs='*', type=str, - help='Space-separated argument list to be passed to the scripts. Note that the ' - 'same arguments will be passed to all named scripts.', + "--script-args", + nargs="*", + type=str, + help="Space-separated argument list to be passed to the scripts. Note that " + "the same arguments will be passed to all named scripts.", ) parser.add_argument( - '--dir-policy', type=str, - choices=[DirPolicyChoices.NONE, DirPolicyChoices.EACH, DirPolicyChoices.ROOT], - help='Policy of selecting scripts execution directory: ' - 'none - start all scripts in current directory ' - 'each - start all scripts in their directories ' - 'root - start all scripts in BASE_DIR directory ', + "--dir-policy", + type=str, + choices=[ + DirPolicyChoices.NONE, + DirPolicyChoices.EACH, + DirPolicyChoices.ROOT, + ], + help="Policy of selecting scripts execution directory: " + "none - start all scripts in current directory " + "each - start all scripts in their directories " + "root - start all scripts in BASE_DIR directory ", ) parser.add_argument( - '--chdir', type=check_is_directory, - help='If dir-policy option is set to custom, than this option determines script execution directory.', + "--chdir", + type=check_is_directory, + help="If dir-policy option is set to custom, than this option determines " + "script execution directory.", ) @signalcommand def handle(self, *args, **options): - self.check() - self.check_migrations() - NOTICE = self.style.SQL_TABLE NOTICE2 = self.style.SQL_FIELD ERROR = self.style.ERROR ERROR2 = self.style.NOTICE subdirs = [] - scripts = options['script'] + scripts = options["script"] - if not options['noscripts']: - subdirs.append(getattr(settings, 'RUNSCRIPT_SCRIPT_DIR', 'scripts')) - if options['infixtures']: - subdirs.append('fixtures') + if not options["noscripts"]: + subdirs.append(getattr(settings, "RUNSCRIPT_SCRIPT_DIR", "scripts")) + if options["infixtures"]: + subdirs.append("fixtures") verbosity = options["verbosity"] - show_traceback = options['traceback'] - no_traceback = options['no_traceback'] - continue_on_error = options['continue_on_error'] + show_traceback = options["traceback"] + no_traceback = options["no_traceback"] + continue_on_error = options["continue_on_error"] if no_traceback: show_traceback = False else: show_traceback = True - silent = options['silent'] + silent = options["silent"] if silent: verbosity = 0 continue_on_error = True - email_notifications = options['email_notifications'] + email_notifications = options["email_notifications"] if len(subdirs) < 1: print(NOTICE("No subdirs to run left.")) @@ -127,7 +152,7 @@ def handle(self, *args, **options): return def get_directory_from_chdir(): - directory = options['chdir'] or getattr(settings, 'RUNSCRIPT_CHDIR', None) + directory = options["chdir"] or getattr(settings, "RUNSCRIPT_CHDIR", None) try: check_is_directory(directory) except ArgumentTypeError as e: @@ -135,7 +160,9 @@ def get_directory_from_chdir(): return directory def get_directory_basing_on_policy(script_module): - policy = options['dir_policy'] or getattr(settings, 'RUNSCRIPT_CHDIR_POLICY', DirPolicyChoices.NONE) + policy = options["dir_policy"] or getattr( + settings, "RUNSCRIPT_CHDIR_POLICY", DirPolicyChoices.NONE + ) if policy == DirPolicyChoices.ROOT: return settings.BASE_DIR elif policy == DirPolicyChoices.EACH: @@ -144,11 +171,11 @@ def get_directory_basing_on_policy(script_module): return self.current_directory def set_directory(script_module): - if options['chdir']: + if options["chdir"]: directory = get_directory_from_chdir() - elif options['dir_policy']: + elif options["dir_policy"]: directory = get_directory_basing_on_policy(script_module) - elif getattr(settings, 'RUNSCRIPT_CHDIR', None): + elif getattr(settings, "RUNSCRIPT_CHDIR", None): directory = get_directory_from_chdir() else: directory = get_directory_basing_on_policy(script_module) @@ -165,13 +192,20 @@ def run_script(mod, *script_args): if isinstance(exit_code, int): if exit_code != 0: try: - raise CommandError("'%s' failed with exit code %s" % (mod.__name__, exit_code), returncode=exit_code) + raise CommandError( + "'%s' failed with exit code %s" + % (mod.__name__, exit_code), + returncode=exit_code, + ) except TypeError: - raise CommandError("'%s' failed with exit code %s" % (mod.__name__, exit_code)) + raise CommandError( + "'%s' failed with exit code %s" + % (mod.__name__, exit_code) + ) if email_notifications: self.send_email_notification(notification_id=mod.__name__) except Exception as e: - if isinstance(e, CommandError) and hasattr(e, 'returncode'): + if isinstance(e, CommandError) and hasattr(e, "returncode"): exit_code = e.returncode self.last_exit_code = exit_code if isinstance(exit_code, int) else 1 if silent: @@ -183,7 +217,9 @@ def run_script(mod, *script_args): traceback.print_exc() return if email_notifications: - self.send_email_notification(notification_id=mod.__name__, include_traceback=True) + self.send_email_notification( + notification_id=mod.__name__, include_traceback=True + ) if no_traceback: raise CommandError(repr(e)) @@ -198,7 +234,7 @@ def my_import(parent_package, module_name): try: importlib.import_module(parent_package) except ImportError as e: - if str(e).startswith('No module named'): + if str(e).startswith("No module named"): # No need to proceed if the parent package doesn't exist return False @@ -210,7 +246,10 @@ def my_import(parent_package, module_name): if importlib.util.find_spec(full_module_path) is None: return False except Exception: - module_file = os.path.join(settings.BASE_DIR, *full_module_path.split('.')) + '.py' + module_file = ( + os.path.join(settings.BASE_DIR, *full_module_path.split(".")) + + ".py" + ) if not os.path.isfile(module_file): return False @@ -219,7 +258,9 @@ def my_import(parent_package, module_name): if show_traceback: traceback.print_exc() if verbosity > 0: - print(ERROR("Cannot import module '%s': %s." % (full_module_path, e))) + print( + ERROR("Cannot import module '%s': %s." % (full_module_path, e)) + ) return False @@ -229,10 +270,15 @@ def my_import(parent_package, module_name): return t else: if verbosity > 1: - print(ERROR2("Found script '%s' but no run() function found." % full_module_path)) + print( + ERROR2( + "Found script '%s' but no run() function found." + % full_module_path + ) + ) def find_modules_for_script(script): - """ Find script module which contains 'run' attribute """ + """Find script module which contains 'run' attribute""" modules = [] # first look in apps for app in apps.get_app_configs(): @@ -255,8 +301,8 @@ def find_modules_for_script(script): return modules - if options['script_args']: - script_args = options['script_args'] + if options["script_args"]: + script_args = options["script_args"] else: script_args = [] @@ -273,7 +319,9 @@ def find_modules_for_script(script): if self.last_exit_code: if verbosity < 2 and not silent: - print(ERROR("Try running with a higher verbosity level like: -v2 or -v3")) + print( + ERROR("Try running with a higher verbosity level like: -v2 or -v3") + ) if not continue_on_error: script_to_run = [] @@ -284,18 +332,24 @@ def find_modules_for_script(script): if self.last_exit_code != 0: if silent: - if hasattr(self, 'running_tests'): + if hasattr(self, "running_tests"): return sys.exit(self.last_exit_code) try: - raise CommandError("An error has occurred running scripts. See errors above.", returncode=self.last_exit_code) + raise CommandError( + "An error has occurred running scripts. See errors above.", + returncode=self.last_exit_code, + ) except TypeError: # Django < 3.1 fallback if self.last_exit_code == 1: - # if exit_code is 1 we can still raise CommandError without returncode argument - raise CommandError("An error has occurred running scripts. See errors above.") + # if exit_code is 1 we can still raise CommandError without + # returncode argument + raise CommandError( + "An error has occurred running scripts. See errors above." + ) print(ERROR("An error has occurred running scripts. See errors above.")) - if hasattr(self, 'running_tests'): + if hasattr(self, "running_tests"): return sys.exit(self.last_exit_code) diff --git a/django_extensions/management/commands/runserver_plus.py b/django_extensions/management/commands/runserver_plus.py index 2c9af4384..f8667720a 100644 --- a/django_extensions/management/commands/runserver_plus.py +++ b/django_extensions/management/commands/runserver_plus.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import logging import os import re @@ -7,7 +6,8 @@ import traceback import webbrowser import functools -from typing import Set +from pathlib import Path +from typing import List, Set # NOQA import django from django.conf import settings @@ -15,14 +15,16 @@ from django.core.management.color import color_style from django.core.servers.basehttp import get_internal_wsgi_application from django.dispatch import Signal -from django.utils.autoreload import get_reloader +from django.template.autoreload import get_template_directories, reset_loaders +from django.utils.autoreload import file_changed, get_reloader from django.views import debug as django_views_debug try: - if 'whitenoise.runserver_nostatic' in settings.INSTALLED_APPS: + if "whitenoise.runserver_nostatic" in settings.INSTALLED_APPS: USE_STATICFILES = False else: from django.contrib.staticfiles.handlers import StaticFilesHandler + USE_STATICFILES = True except ImportError: USE_STATICFILES = False @@ -34,30 +36,41 @@ from werkzeug.serving import make_ssl_devcert from werkzeug._internal import _log # type: ignore from werkzeug import _reloader + HAS_WERKZEUG = True except ImportError: HAS_WERKZEUG = False try: import OpenSSL # NOQA + HAS_OPENSSL = True except ImportError: HAS_OPENSSL = False from django_extensions.management.technical_response import null_technical_500_response -from django_extensions.management.utils import RedirectHandler, has_ipdb, setup_logger, signalcommand +from django_extensions.management.utils import ( + RedirectHandler, + has_ipdb, + setup_logger, + signalcommand, +) from django_extensions.management.debug_cursor import monkey_patch_cursordebugwrapper runserver_plus_started = Signal() -naiveip_re = re.compile(r"""^(?: +naiveip_re = re.compile( + r"""^(?: (?P (?P\d{1,3}(?:\.\d{1,3}){3}) | # IPv4 address (?P\[[a-fA-F0-9:]+\]) | # IPv6 address (?P[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN -):)?(?P\d+)$""", re.X) +):)?(?P\d+)$""", + re.X, +) # 7-bit C1 ANSI sequences (https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python) -ansi_escape = re.compile(r''' +ansi_escape = re.compile( + r""" \x1B # ESC (?: # 7-bit C1 Fe (except CSI) [@-Z\\-_] @@ -67,31 +80,83 @@ [ -/]* # Intermediate bytes [@-~] # Final byte ) -''', re.VERBOSE) +""", + re.VERBOSE, +) DEFAULT_PORT = "8000" -DEFAULT_POLLER_RELOADER_INTERVAL = getattr(settings, 'RUNSERVERPLUS_POLLER_RELOADER_INTERVAL', 1) -DEFAULT_POLLER_RELOADER_TYPE = getattr(settings, 'RUNSERVERPLUS_POLLER_RELOADER_TYPE', 'auto') +DEFAULT_POLLER_RELOADER_INTERVAL = getattr( + settings, "RUNSERVERPLUS_POLLER_RELOADER_INTERVAL", 1 +) +DEFAULT_POLLER_RELOADER_TYPE = getattr( + settings, "RUNSERVERPLUS_POLLER_RELOADER_TYPE", "auto" +) logger = logging.getLogger(__name__) _error_files = set() # type: Set[str] +def get_all_template_files() -> Set[str]: + template_list = set() + + for template_dir in get_template_directories(): + for base_dir, _, filenames in os.walk(template_dir): + for filename in filenames: + template_list.add(os.path.join(base_dir, filename)) + + return template_list + + if HAS_WERKZEUG: # Monkey patch the reloader to support adding more files to extra_files for name, reloader_loop_klass in _reloader.reloader_loops.items(): + class WrappedReloaderLoop(reloader_loop_klass): # type: ignore def __init__(self, *args, **kwargs): + self._template_files: Set[str] = get_all_template_files() super().__init__(*args, **kwargs) self._extra_files = self.extra_files @property def extra_files(self): - return self._extra_files.union(_error_files) + template_files = get_all_template_files() + + # reset loaders if there are new files detected + if len(self._template_files) != len(template_files): + changed = template_files.difference(self._template_files) + for filename in changed: + _log( + "info", + f" * New file {filename} added, reset template loaders", + ) + self.register_file_changed(filename) + + reset_loaders() + + self._template_files = template_files + + return self._extra_files.union(_error_files, template_files) @extra_files.setter def extra_files(self, extra_files): self._extra_files = extra_files + def trigger_reload(self, filename: str) -> None: + path = Path(filename) + results = file_changed.send(sender=self, file_path=path) + if not any(res[1] for res in results): + super().trigger_reload(filename) + else: + _log( + "info", + f" * Detected change in {filename!r}, reset template loaders", + ) + self.register_file_changed(filename) + + def register_file_changed(self, filename): + if hasattr(self, "mtimes"): + mtime = os.stat(filename).st_mtime + self.mtimes[filename] = mtime + _reloader.reloader_loops[name] = WrappedReloaderLoop @@ -110,7 +175,7 @@ def wrapper(*args, **kwargs): _, ev, tb = _exception - if getattr(ev, 'filename', None) is None: + if getattr(ev, "filename", None) is None: # get the filename from the last item in the stack filename = traceback.extract_tb(tb)[-1][0] else: @@ -128,106 +193,245 @@ 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" def add_arguments(self, parser): super().add_arguments(parser) - parser.add_argument('addrport', nargs='?', - help='Optional port number, or ipaddr:port') - parser.add_argument('--ipv6', '-6', action='store_true', dest='use_ipv6', default=False, - help='Tells Django to use a IPv6 address.') - parser.add_argument('--noreload', action='store_false', dest='use_reloader', default=True, - help='Tells Django to NOT use the auto-reloader.') - parser.add_argument('--browser', action='store_true', dest='open_browser', - help='Tells Django to open a browser.') - parser.add_argument('--nothreading', action='store_false', dest='threaded', - help='Do not run in multithreaded mode.') - parser.add_argument('--threaded', action='store_true', dest='threaded', - help='Run in multithreaded mode.') - parser.add_argument('--output', dest='output_file', default=None, - 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") + parser.add_argument( + "addrport", nargs="?", help="Optional port number, or ipaddr:port" + ) + parser.add_argument( + "--ipv6", + "-6", + action="store_true", + dest="use_ipv6", + default=False, + help="Tells Django to use a IPv6 address.", + ) + parser.add_argument( + "--noreload", + action="store_false", + dest="use_reloader", + default=True, + help="Tells Django to NOT use the auto-reloader.", + ) + parser.add_argument( + "--browser", + action="store_true", + dest="open_browser", + help="Tells Django to open a browser.", + ) + parser.add_argument( + "--nothreading", + action="store_false", + dest="threaded", + help="Do not run in multithreaded mode.", + ) + parser.add_argument( + "--threaded", + action="store_true", + dest="threaded", + help="Run in multithreaded mode.", + ) + parser.add_argument( + "--output", + dest="output_file", + default=None, + 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() - cert_group.add_argument('--cert', dest='cert_path', action="store", type=str, - help='Deprecated alias for --cert-file option.') - cert_group.add_argument('--cert-file', dest='cert_path', action="store", type=str, - help='SSL .crt file path. If not provided path from --key-file will be selected. ' - 'Either --cert-file or --key-file must be provided to use SSL.') - parser.add_argument('--key-file', dest='key_file_path', action="store", type=str, - help='SSL .key file path. If not provided path from --cert-file will be selected. ' - '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('--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, - help='Werkzeug reloader type [options are auto, watchdog, or stat, default=%s]' % DEFAULT_POLLER_RELOADER_TYPE) - parser.add_argument('--pdb', action='store_true', dest='pdb', default=False, - help='Drop into pdb shell at the start of any view.') - parser.add_argument('--ipdb', action='store_true', dest='ipdb', default=False, - help='Drop into ipdb shell at the start of any view.') - parser.add_argument('--pm', action='store_true', dest='pm', default=False, - help='Drop into (i)pdb shell if an exception is raised in a view.') - parser.add_argument('--startup-messages', dest='startup_messages', action="store", default='reload', - help='When to show startup messages: reload [default], once, always, never.') - parser.add_argument('--keep-meta-shutdown', dest='keep_meta_shutdown_func', action='store_true', default=False, - help="Keep request.META['werkzeug.server.shutdown'] function which is automatically removed " - "because Django debug pages tries to call the function and unintentionally shuts down " - "the Werkzeug server.") - parser.add_argument("--nopin", dest="nopin", action="store_true", default=False, - help="Disable the PIN in werkzeug. USE IT WISELY!") + cert_group.add_argument( + "--cert", + dest="cert_path", + action="store", + type=str, + help="Deprecated alias for --cert-file option.", + ) + cert_group.add_argument( + "--cert-file", + dest="cert_path", + action="store", + type=str, + help="SSL .crt file path. If not provided path from --key-file will be " + "selected. Either --cert-file or --key-file must be provided to use SSL.", + ) + parser.add_argument( + "--key-file", + dest="key_file_path", + action="store", + type=str, + help="SSL .key file path. If not provided path from --cert-file " + "will be selected. 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, + help="Werkzeug reloader type " + "[options are auto, watchdog, or stat, default=%s]" + % DEFAULT_POLLER_RELOADER_TYPE, + ) + parser.add_argument( + "--pdb", + action="store_true", + dest="pdb", + default=False, + help="Drop into pdb shell at the start of any view.", + ) + parser.add_argument( + "--ipdb", + action="store_true", + dest="ipdb", + default=False, + help="Drop into ipdb shell at the start of any view.", + ) + parser.add_argument( + "--pm", + action="store_true", + dest="pm", + default=False, + help="Drop into (i)pdb shell if an exception is raised in a view.", + ) + parser.add_argument( + "--startup-messages", + dest="startup_messages", + action="store", + default="reload", + help="When to show startup messages: " + "reload [default], once, always, never.", + ) + parser.add_argument( + "--keep-meta-shutdown", + dest="keep_meta_shutdown_func", + action="store_true", + default=False, + help="Keep request.META['werkzeug.server.shutdown'] function which is " + "automatically removed because Django debug pages tries to call the " + "function and unintentionally shuts down the Werkzeug server.", + ) + parser.add_argument( + "--nopin", + dest="nopin", + action="store_true", + default=False, + help="Disable the PIN in werkzeug. USE IT WISELY!", + ) if USE_STATICFILES: - parser.add_argument('--nostatic', action="store_false", dest='use_static_handler', default=True, - help='Tells Django to NOT automatically serve static files at STATIC_URL.') - parser.add_argument('--insecure', action="store_true", dest='insecure_serving', default=False, - help='Allows serving static files even if DEBUG is False.') + parser.add_argument( + "--nostatic", + action="store_false", + dest="use_static_handler", + default=True, + help="Tells Django to NOT automatically serve static files.", + ) + parser.add_argument( + "--insecure", + action="store_true", + dest="insecure_serving", + default=False, + help="Allows serving static files even if DEBUG is False.", + ) @signalcommand def handle(self, *args, **options): - addrport = options['addrport'] - startup_messages = options['startup_messages'] + addrport = options["addrport"] + startup_messages = options["startup_messages"] if startup_messages == "reload": - self.show_startup_messages = os.environ.get('RUNSERVER_PLUS_SHOW_MESSAGES') + self.show_startup_messages = os.environ.get("RUNSERVER_PLUS_SHOW_MESSAGES") elif startup_messages == "once": - self.show_startup_messages = not os.environ.get('RUNSERVER_PLUS_SHOW_MESSAGES') + self.show_startup_messages = not os.environ.get( + "RUNSERVER_PLUS_SHOW_MESSAGES" + ) elif startup_messages == "never": self.show_startup_messages = False else: self.show_startup_messages = True - os.environ['RUNSERVER_PLUS_SHOW_MESSAGES'] = '1' + os.environ["RUNSERVER_PLUS_SHOW_MESSAGES"] = "1" - setup_logger(logger, self.stderr, filename=options['output_file']) # , fmt="[%(name)s] %(message)s") + setup_logger( + logger, self.stderr, filename=options["output_file"] + ) # , fmt="[%(name)s] %(message)s") logredirect = RedirectHandler(__name__) # Redirect werkzeug log items - werklogger = logging.getLogger('werkzeug') + werklogger = logging.getLogger("werkzeug") werklogger.setLevel(logging.INFO) werklogger.addHandler(logredirect) werklogger.propagate = False - pdb_option = options['pdb'] - ipdb_option = options['ipdb'] - pm = options['pm'] + pdb_option = options["pdb"] + ipdb_option = options["ipdb"] + pm = options["pm"] try: from django_pdb.middleware import PdbMiddleware except ImportError: if pdb_option or ipdb_option or pm: - raise CommandError("django-pdb is required for --pdb, --ipdb and --pm options. Please visit https://pypi.python.org/pypi/django-pdb or install via pip. (pip install django-pdb)") + raise CommandError( + "django-pdb is required for --pdb, --ipdb and --pm options. " + "Please visit https://pypi.python.org/pypi/django-pdb or install " + "via pip. (pip install django-pdb)" + ) pm = False else: # Add pdb middleware if --pdb is specified or if in DEBUG mode - if (pdb_option or ipdb_option or settings.DEBUG): - middleware = 'django_pdb.middleware.PdbMiddleware' - settings_middleware = getattr(settings, 'MIDDLEWARE', None) or settings.MIDDLEWARE_CLASSES + if pdb_option or ipdb_option or settings.DEBUG: + middleware = "django_pdb.middleware.PdbMiddleware" + settings_middleware = ( + getattr(settings, "MIDDLEWARE", None) or settings.MIDDLEWARE_CLASSES + ) if middleware not in settings_middleware: if isinstance(settings_middleware, tuple): @@ -238,26 +442,32 @@ def handle(self, *args, **options): # If --pdb is specified then always break at the start of views. # Otherwise break only if a 'pdb' query parameter is set in the url if pdb_option: - PdbMiddleware.always_break = 'pdb' + PdbMiddleware.always_break = "pdb" elif ipdb_option: - PdbMiddleware.always_break = 'ipdb' + PdbMiddleware.always_break = "ipdb" def postmortem(request, exc_type, exc_value, tb): if has_ipdb(): import ipdb + p = ipdb else: import pdb + p = pdb - print("Exception occured: %s, %s" % (exc_type, exc_value), file=sys.stderr) + print( + "Exception occured: %s, %s" % (exc_type, exc_value), file=sys.stderr + ) p.post_mortem(tb) # usurp django's handler - django_views_debug.technical_500_response = postmortem if pm else null_technical_500_response + django_views_debug.technical_500_response = ( + postmortem if pm else null_technical_500_response + ) - self.use_ipv6 = options['use_ipv6'] + self.use_ipv6 = options["use_ipv6"] if self.use_ipv6 and not socket.has_ipv6: - raise CommandError('Your Python does not support IPv6.') + raise CommandError("Your Python does not support IPv6.") self._raw_ipv6 = False if not addrport: try: @@ -265,32 +475,37 @@ def postmortem(request, exc_type, exc_value, tb): except AttributeError: pass if not addrport: - self.addr = '' + self.addr = "" self.port = DEFAULT_PORT else: m = re.match(naiveip_re, addrport) if m is None: - raise CommandError('"%s" is not a valid port number ' - 'or address:port pair.' % addrport) + raise CommandError( + '"%s" is not a valid port number or address:port pair.' % addrport + ) self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups() if not self.port.isdigit(): - raise CommandError("%r is not a valid port number." % - self.port) + raise CommandError("%r is not a valid port number." % self.port) if self.addr: if _ipv6: self.addr = self.addr[1:-1] self.use_ipv6 = True self._raw_ipv6 = True elif self.use_ipv6 and not _fqdn: - raise CommandError('"%s" is not a valid IPv6 address.' - % self.addr) + raise CommandError('"%s" is not a valid IPv6 address.' % self.addr) if not self.addr: - self.addr = '::1' if self.use_ipv6 else '127.0.0.1' + self.addr = "::1" if self.use_ipv6 else "127.0.0.1" self._raw_ipv6 = True 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"): + 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): @@ -300,7 +515,7 @@ def get_handler(self, *args, **options): def get_error_handler(self, exc, **options): def application(env, start_response): if isinstance(exc, SystemCheckError): - error_message = ansi_escape.sub('', str(exc)) + error_message = ansi_escape.sub("", str(exc)) raise SystemCheckError(error_message) raise exc @@ -309,32 +524,48 @@ def application(env, start_response): def inner_run(self, options): if not HAS_WERKZEUG: - raise CommandError("Werkzeug is required to use runserver_plus. Please visit http://werkzeug.pocoo.org/ or install via pip. (pip install Werkzeug)") + raise CommandError( + "Werkzeug is required to use runserver_plus. " + "Please visit https://werkzeug.palletsprojects.com/ or install via pip." + " (pip install Werkzeug)" + ) # Set colored output if settings.DEBUG: try: set_werkzeug_log_color() - except Exception: # We are dealing with some internals, anything could go wrong + except ( + Exception + ): # We are dealing with some internals, anything could go wrong if self.show_startup_messages: - print("Wrapping internal werkzeug logger for color highlighting has failed!") + print( + "Wrapping internal werkzeug logger " + "for color highlighting has failed!" + ) class WSGIRequestHandler(_WSGIRequestHandler): def make_environ(self): environ = super().make_environ() - if not options['keep_meta_shutdown_func']: - del environ['werkzeug.server.shutdown'] + if ( + not options["keep_meta_shutdown_func"] + and "werkzeug.server.shutdown" in environ + ): + del environ["werkzeug.server.shutdown"] + remote_user = os.getenv("REMOTE_USER") + if remote_user is not None: + environ["REMOTE_USER"] = remote_user return environ - threaded = options['threaded'] - use_reloader = options['use_reloader'] - open_browser = options['open_browser'] - quit_command = 'CONTROL-C' if sys.platform != 'win32' else 'CTRL-BREAK' - reloader_interval = options['reloader_interval'] - reloader_type = options['reloader_type'] - self.extra_files = set(options['extra_files']) + threaded = options["threaded"] + use_reloader = options["use_reloader"] + open_browser = options["open_browser"] + quit_command = "CONTROL-C" if sys.platform != "win32" else "CTRL-BREAK" + 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'] + self.nopin = options["nopin"] if self.show_startup_messages: print("Performing system checks...\n") @@ -348,16 +579,18 @@ def make_environ(self): handler = self.get_error_handler(exc, **options) if USE_STATICFILES: - use_static_handler = options['use_static_handler'] - insecure_serving = options['insecure_serving'] + use_static_handler = options["use_static_handler"] + insecure_serving = options["insecure_serving"] if use_static_handler and (settings.DEBUG or insecure_serving): handler = StaticFilesHandler(handler) if options["cert_path"] or options["key_file_path"]: if not HAS_OPENSSL: - raise CommandError("Python OpenSSL Library is " - "required to use runserver_plus with ssl support. " - "Install via pip (pip install pyOpenSSL).") + raise CommandError( + "Python OpenSSL Library is " + "required to use runserver_plus with ssl support. " + "Install via pip (pip install pyOpenSSL)." + ) certfile, keyfile = self.determine_ssl_files_paths(options) dir_path, root = os.path.split(certfile) @@ -366,43 +599,59 @@ def make_environ(self): if os.path.exists(certfile) and os.path.exists(keyfile): ssl_context = (certfile, keyfile) else: # Create cert, key files ourselves. - ssl_context = make_ssl_devcert(os.path.join(dir_path, root), host='localhost') + ssl_context = make_ssl_devcert( + os.path.join(dir_path, root), host="localhost" + ) except ImportError: if self.show_startup_messages: - print("Werkzeug version is less than 0.9, trying adhoc certificate.") + print( + "Werkzeug version is less than 0.9, trying adhoc certificate." + ) ssl_context = "adhoc" else: ssl_context = None bind_url = "%s://%s:%s/" % ( - "https" if ssl_context else "http", self.addr if not self._raw_ipv6 else '[%s]' % self.addr, self.port) + "https" if ssl_context else "http", + self.addr if not self._raw_ipv6 else "[%s]" % self.addr, + self.port, + ) if self.show_startup_messages: - print("\nDjango version %s, using settings %r" % (django.get_version(), settings.SETTINGS_MODULE)) + print( + "\nDjango version %s, using settings %r" + % (django.get_version(), settings.SETTINGS_MODULE) + ) print("Development server is running at %s" % (bind_url,)) - print("Using the Werkzeug debugger (http://werkzeug.pocoo.org/)") + print("Using the Werkzeug debugger (https://werkzeug.palletsprojects.com/)") print("Quit the server with %s." % quit_command) if open_browser: webbrowser.open(bind_url) if use_reloader and settings.USE_I18N: - self.extra_files |= set(filter(lambda filename: str(filename).endswith('.mo'), gen_filenames())) + self.extra_files |= set( + filter(lambda filename: str(filename).endswith(".mo"), gen_filenames()) + ) - if getattr(settings, 'RUNSERVER_PLUS_EXTRA_FILES', []): + 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 if not use_reloader: - os.environ['WERKZEUG_RUN_MAIN'] = 'true' + os.environ["WERKZEUG_RUN_MAIN"] = "true" # Don't run a second instance of the debugger / reloader # See also: https://github.com/django-extensions/django-extensions/issues/832 - if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': + if os.environ.get("WERKZEUG_RUN_MAIN") != "true": if self.nopin: - os.environ['WERKZEUG_DEBUG_PIN'] = 'off' + os.environ["WERKZEUG_DEBUG_PIN"] = "off" handler = DebuggedApplication(handler, True) runserver_plus_started.send(sender=self) @@ -413,6 +662,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, @@ -422,22 +672,36 @@ def make_environ(self): @classmethod def determine_ssl_files_paths(cls, options): - key_file_path = options.get('key_file_path') or "" - cert_path = options.get('cert_path') or "" - cert_file = cls._determine_path_for_file(cert_path, key_file_path, cls.DEFAULT_CRT_EXTENSION) - key_file = cls._determine_path_for_file(key_file_path, cert_path, cls.DEFAULT_KEY_EXTENSION) + key_file_path = os.path.expanduser(options.get("key_file_path") or "") + cert_path = os.path.expanduser(options.get("cert_path") or "") + cert_file = cls._determine_path_for_file( + cert_path, key_file_path, cls.DEFAULT_CRT_EXTENSION + ) + key_file = cls._determine_path_for_file( + key_file_path, cert_path, cls.DEFAULT_KEY_EXTENSION + ) return cert_file, key_file @classmethod - def _determine_path_for_file(cls, current_file_path, other_file_path, expected_extension): - directory = cls._get_directory_basing_on_file_paths(current_file_path, other_file_path) - file_name = cls._get_file_name(current_file_path) or cls._get_file_name(other_file_path) + def _determine_path_for_file( + cls, current_file_path, other_file_path, expected_extension + ): + directory = cls._get_directory_basing_on_file_paths( + current_file_path, other_file_path + ) + file_name = cls._get_file_name(current_file_path) or cls._get_file_name( + other_file_path + ) extension = cls._get_extension(current_file_path) or expected_extension return os.path.join(directory, file_name + extension) @classmethod def _get_directory_basing_on_file_paths(cls, current_file_path, other_file_path): - return cls._get_directory(current_file_path) or cls._get_directory(other_file_path) or os.getcwd() + return ( + cls._get_directory(current_file_path) + or cls._get_directory(other_file_path) + or os.getcwd() + ) @classmethod def _get_directory(cls, file_path): @@ -459,7 +723,7 @@ def set_werkzeug_log_color(): def werk_log(self, type, message, *args): try: - msg = '%s - - [%s] %s' % ( + msg = "%s - - [%s] %s" % ( self.address_string(), self.log_date_time_string(), message % args, @@ -469,18 +733,18 @@ def werk_log(self, type, message, *args): return _orig_log(type, message, *args) # Utilize terminal colors, if available - if http_code[0] == '2': + if http_code[0] == "2": # Put 2XX first, since it should be the common case msg = _style.HTTP_SUCCESS(msg) - elif http_code[0] == '1': + elif http_code[0] == "1": msg = _style.HTTP_INFO(msg) - elif http_code == '304': + elif http_code == "304": msg = _style.HTTP_NOT_MODIFIED(msg) - elif http_code[0] == '3': + elif http_code[0] == "3": msg = _style.HTTP_REDIRECT(msg) - elif http_code == '404': + elif http_code == "404": msg = _style.HTTP_NOT_FOUND(msg) - elif http_code[0] == '4': + elif http_code[0] == "4": msg = _style.HTTP_BAD_REQUEST(msg) else: # Any 5XX, or any other response diff --git a/django_extensions/management/commands/set_default_site.py b/django_extensions/management/commands/set_default_site.py index 1ea5fc273..4629888f8 100644 --- a/django_extensions/management/commands/set_default_site.py +++ b/django_extensions/management/commands/set_default_site.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import socket from django.conf import settings @@ -14,39 +13,44 @@ class Command(BaseCommand): def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--name', dest='site_name', default=None, - help='Use this as site name.' + "--name", dest="site_name", default=None, help="Use this as site name." ) parser.add_argument( - '--domain', dest='site_domain', default=None, - help='Use this as site domain.' + "--domain", + dest="site_domain", + default=None, + help="Use this as site domain.", ) parser.add_argument( - '--system-fqdn', dest='set_as_system_fqdn', default=False, + "--system-fqdn", + dest="set_as_system_fqdn", + default=False, action="store_true", - help='Use the systems FQDN (Fully Qualified Domain Name) as name ' - 'and domain. Can be used in combination with --name' + help="Use the systems FQDN (Fully Qualified Domain Name) as name " + "and domain. Can be used in combination with --name", ) @signalcommand def handle(self, *args, **options): - if not apps.is_installed('django.contrib.sites'): - raise CommandError('The sites framework is not installed.') + if not apps.is_installed("django.contrib.sites"): + raise CommandError("The sites framework is not installed.") from django.contrib.sites.models import Site try: site = Site.objects.get(pk=settings.SITE_ID) except Site.DoesNotExist: - raise CommandError("Default site with pk=%s does not exist" % - settings.SITE_ID) + raise CommandError( + "Default site with pk=%s does not exist" % settings.SITE_ID + ) else: name = options["site_name"] domain = options["site_domain"] set_as_system_fqdn = options["set_as_system_fqdn"] if all([domain, set_as_system_fqdn]): raise CommandError( - "The set_as_system_fqdn cannot be used with domain option.") # noqa + "The set_as_system_fqdn cannot be used with domain option." + ) # noqa if set_as_system_fqdn: domain = socket.getfqdn() if not domain: @@ -62,10 +66,12 @@ def handle(self, *args, **options): update_kwargs["domain"] = domain if update_kwargs: - Site.objects.filter( - pk=settings.SITE_ID).update(**update_kwargs) + Site.objects.filter(pk=settings.SITE_ID).update(**update_kwargs) site = Site.objects.get(pk=settings.SITE_ID) - print("Updated default site. You might need to restart django as sites are cached aggressively.") + print( + "Updated default site. You might need to restart django as sites" + " are cached aggressively." + ) else: print("Nothing to update (need --name, --domain and/or --system-fqdn)") diff --git a/django_extensions/management/commands/set_fake_emails.py b/django_extensions/management/commands/set_fake_emails.py index 4eb4a9c8b..269d13722 100644 --- a/django_extensions/management/commands/set_fake_emails.py +++ b/django_extensions/management/commands/set_fake_emails.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ set_fake_emails.py @@ -8,63 +7,94 @@ """ +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 from django_extensions.management.utils import signalcommand -DEFAULT_FAKE_EMAIL = '%(username)s@example.com' +DEFAULT_FAKE_EMAIL = "%(username)s@example.com" 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 + 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: List[str] = [] def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--email', dest='default_email', default=DEFAULT_FAKE_EMAIL, - help='Use this as the new email format.' + "--email", + dest="default_email", + default=DEFAULT_FAKE_EMAIL, + help="Use this as the new email format.", ) parser.add_argument( - '-a', '--no-admin', action="store_true", dest='no_admin', - default=False, help='Do not change administrator accounts' + "-a", + "--no-admin", + action="store_true", + dest="no_admin", + default=False, + help="Do not change administrator accounts", ) parser.add_argument( - '-s', '--no-staff', action="store_true", dest='no_staff', - default=False, help='Do not change staff accounts' + "-s", + "--no-staff", + action="store_true", + dest="no_staff", + default=False, + help="Do not change staff accounts", ) parser.add_argument( - '--include', dest='include_regexp', default=None, - help='Include usernames matching this regexp.' + "--include", + dest="include_regexp", + default=None, + help="Include usernames matching this regexp.", ) parser.add_argument( - '--exclude', dest='exclude_regexp', default=None, - help='Exclude usernames matching this regexp.' + "--exclude", + dest="exclude_regexp", + default=None, + help="Exclude usernames matching this regexp.", ) parser.add_argument( - '--include-groups', dest='include_groups', default=None, - help='Include users matching this group. (use comma seperation for multiple groups)' + "--include-groups", + dest="include_groups", + default=None, + help=( + "Include users matching this group. " + "(use comma separation for multiple groups)" + ), ) parser.add_argument( - '--exclude-groups', dest='exclude_groups', default=None, - help='Exclude users matching this group. (use comma seperation for multiple groups)' + "--exclude-groups", + dest="exclude_groups", + default=None, + help=( + "Exclude users matching this group. " + "(use comma separation for multiple groups)" + ), ) @signalcommand def handle(self, *args, **options): if not settings.DEBUG: - raise CommandError('Only available in debug mode') + raise CommandError("Only available in debug mode") from django.contrib.auth.models import Group - email = options['default_email'] - include_regexp = options['include_regexp'] - exclude_regexp = options['exclude_regexp'] - include_groups = options['include_groups'] - exclude_groups = options['exclude_groups'] - no_admin = options['no_admin'] - no_staff = options['no_staff'] + + email = options["default_email"] + include_regexp = options["include_regexp"] + exclude_regexp = options["exclude_regexp"] + include_groups = options["include_groups"] + exclude_groups = options["exclude_groups"] + no_admin = options["no_admin"] + no_staff = options["no_staff"] User = get_user_model() users = User.objects.all() @@ -89,8 +119,10 @@ def handle(self, *args, **options): if include_regexp: users = users.filter(username__regex=include_regexp) for user in users: - user.email = email % {'username': user.username, - 'first_name': user.first_name, - 'last_name': user.last_name} + user.email = email % { + "username": user.username, + "first_name": user.first_name, + "last_name": user.last_name, + } user.save() - print('Changed %d emails' % users.count()) + print("Changed %d emails" % users.count()) diff --git a/django_extensions/management/commands/set_fake_passwords.py b/django_extensions/management/commands/set_fake_passwords.py index 5d619bd02..b82208994 100644 --- a/django_extensions/management/commands/set_fake_passwords.py +++ b/django_extensions/management/commands/set_fake_passwords.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ set_fake_passwords.py @@ -7,47 +6,57 @@ 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 from django_extensions.management.utils import signalcommand -DEFAULT_FAKE_PASSWORD = 'password' +DEFAULT_FAKE_PASSWORD = "password" class Command(BaseCommand): - help = 'DEBUG only: sets all user passwords to a common value ("%s" by default)' % (DEFAULT_FAKE_PASSWORD, ) - requires_system_checks = False + help = 'DEBUG only: sets all user passwords to a common value ("%s" by default)' % ( + DEFAULT_FAKE_PASSWORD, + ) + requires_system_checks: List[str] = [] def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--prompt', dest='prompt_passwd', default=False, - action='store_true', - help='Prompts for the new password to apply to all users' + "--prompt", + dest="prompt_passwd", + default=False, + action="store_true", + help="Prompts for the new password to apply to all users", ) parser.add_argument( - '--password', dest='default_passwd', default=DEFAULT_FAKE_PASSWORD, - help='Use this as default password.' + "--password", + dest="default_passwd", + default=DEFAULT_FAKE_PASSWORD, + help="Use this as default password.", ) @signalcommand def handle(self, *args, **options): if not settings.DEBUG: - raise CommandError('Only available in debug mode') + raise CommandError("Only available in debug mode") - if options['prompt_passwd']: + if options["prompt_passwd"]: from getpass import getpass - passwd = getpass('Password: ') + + passwd = getpass("Password: ") if not passwd: - raise CommandError('You must enter a valid password') + raise CommandError("You must enter a valid password") else: - passwd = options['default_passwd'] + passwd = options["default_passwd"] User = get_user_model() user = User() user.set_password(passwd) count = User.objects.all().update(password=user.password) - print('Reset %d passwords' % count) + print("Reset %d passwords" % count) diff --git a/django_extensions/management/commands/shell_plus.py b/django_extensions/management/commands/shell_plus.py index dcb991174..d1367e0d1 100644 --- a/django_extensions/management/commands/shell_plus.py +++ b/django_extensions/management/commands/shell_plus.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import inspect import os import sys @@ -16,11 +15,11 @@ def use_vi_mode(): - editor = os.environ.get('EDITOR') + editor = os.environ.get("EDITOR") if not editor: return False editor = os.path.basename(editor) - return editor.startswith('vi') or editor.endswith('vim') + return editor.startswith("vi") or editor.endswith("vim") def shell_runner(flags, name, help=None): @@ -30,7 +29,7 @@ def shell_runner(flags, name, help=None): :param flags: The flags used to start this runner via the ArgumentParser. :param name: The name of this runner for the help text for the ArgumentParser. :param help: The optional help for the ArgumentParser if the dynamically generated help is not sufficient. - """ + """ # noqa: E501 def decorator(fn): fn.runner_flags = flags @@ -43,14 +42,17 @@ def decorator(fn): class Command(BaseCommand): - help = "Like the 'shell' command but autoloads the models of all installed Django apps." + help = "Like the 'shell' command but autoloads the models of all installed Django apps." # noqa: E501 extra_args = None tests_mode = False def __init__(self): super().__init__() - self.runners = [member for name, member in inspect.getmembers(self) - if hasattr(member, 'runner_flags')] + self.runners = [ + member + for name, member in inspect.getmembers(self) + if hasattr(member, "runner_flags") + ] def add_arguments(self, parser): super().add_arguments(parser) @@ -60,82 +62,116 @@ def add_arguments(self, parser): if runner.runner_help: help = runner.runner_help else: - help = 'Tells Django to use %s.' % runner.runner_name + help = "Tells Django to use %s." % runner.runner_name group.add_argument( - *runner.runner_flags, action='store_const', dest='runner', const=runner, help=help) + *runner.runner_flags, + action="store_const", + dest="runner", + const=runner, + help=help, + ) parser.add_argument( - '--connection-file', action='store', dest='connection_file', - help='Specifies the connection file to use if using the --kernel option' + "--connection-file", + action="store", + dest="connection_file", + help="Specifies the connection file to use if using the --kernel option", ) parser.add_argument( - '--no-startup', action='store_true', dest='no_startup', + "--no-startup", + action="store_true", + dest="no_startup", default=False, - help='When using plain Python, ignore the PYTHONSTARTUP environment variable and ~/.pythonrc.py script.' + help=( + "When using plain Python, ignore the PYTHONSTARTUP environment " + "variable and ~/.pythonrc.py script." + ), ) parser.add_argument( - '--use-pythonrc', action='store_true', dest='use_pythonrc', + "--use-pythonrc", + action="store_true", + dest="use_pythonrc", default=False, - help='When using plain Python, load the PYTHONSTARTUP environment variable and ~/.pythonrc.py script.' + help=( + "When using plain Python, load the PYTHONSTARTUP environment variable " + "and ~/.pythonrc.py script." + ), ) parser.add_argument( - '--print-sql', action='store_true', + "--print-sql", + action="store_true", default=False, - help="Print SQL queries as they're executed" + 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." + "--truncate-sql", + action="store", + type=int, + help="Truncate SQL queries to a number of characters.", ) parser.add_argument( - '--print-sql-location', action='store_true', + "--print-sql-location", + action="store_true", default=False, - help="Show location in code where SQL query generated from" + help="Show location in code where SQL query generated from", ) parser.add_argument( - '--dont-load', action='append', dest='dont_load', default=[], - help='Ignore autoloading of some apps/models. Can be used several times.' + "--dont-load", + action="append", + dest="dont_load", + default=[], + help="Ignore autoloading of some apps/models. Can be used several times.", ) parser.add_argument( - '--quiet-load', action='store_true', + "--quiet-load", + action="store_true", default=False, - dest='quiet_load', help='Do not display loaded models messages' + dest="quiet_load", + help="Do not display loaded models messages", ) parser.add_argument( - '--vi', action='store_true', default=use_vi_mode(), dest='vi_mode', - help='Load Vi key bindings (for --ptpython and --ptipython)' + "--vi", + action="store_true", + default=use_vi_mode(), + dest="vi_mode", + help="Load Vi key bindings (for --ptpython and --ptipython)", ) parser.add_argument( - '--no-browser', action='store_true', + "--no-browser", + action="store_true", default=False, - dest='no_browser', - help='Don\'t open the notebook in a browser after startup.' + dest="no_browser", + help="Don't open the notebook in a browser after startup.", ) parser.add_argument( - '-c', '--command', - help='Instead of opening an interactive shell, run a command as Django and exit.', + "-c", + "--command", + help=( + "Instead of opening an interactive shell, " + "run a command as Django and exit." + ), ) def run_from_argv(self, argv): - if '--' in argv[2:]: - idx = argv.index('--') - self.extra_args = argv[idx + 1:] + if "--" in argv[2:]: + idx = argv.index("--") + self.extra_args = argv[idx + 1 :] argv = argv[:idx] return super().run_from_argv(argv) def get_ipython_arguments(self, options): - ipython_args = 'IPYTHON_ARGUMENTS' + ipython_args = "IPYTHON_ARGUMENTS" arguments = getattr(settings, ipython_args, []) if not arguments: - arguments = os.environ.get(ipython_args, '').split() + arguments = os.environ.get(ipython_args, "").split() return arguments def get_notebook_arguments(self, options): - notebook_args = 'NOTEBOOK_ARGUMENTS' + notebook_args = "NOTEBOOK_ARGUMENTS" arguments = getattr(settings, notebook_args, []) if not arguments: - arguments = os.environ.get(notebook_args, '').split() + arguments = os.environ.get(notebook_args, "").split() return arguments def get_imported_objects(self, options): @@ -145,12 +181,15 @@ def get_imported_objects(self, options): self.tests_imported_objects = imported_objects return imported_objects - @shell_runner(flags=['--kernel'], name='IPython Kernel') + @shell_runner(flags=["--kernel"], name="IPython Kernel") def get_kernel(self, options): try: from IPython import release + if release.version_info[0] < 2: - print(self.style.ERROR("--kernel requires at least IPython version 2.0")) + print( + self.style.ERROR("--kernel requires at least IPython version 2.0") + ) return from IPython import start_kernel except ImportError: @@ -162,19 +201,24 @@ def run_kernel(): argv=[], user_ns=imported_objects, ) - connection_file = options['connection_file'] + connection_file = options["connection_file"] if connection_file: - kwargs['connection_file'] = connection_file + kwargs["connection_file"] = connection_file start_kernel(**kwargs) + return run_kernel def load_base_kernel_spec(self, app): """Finds and returns the base Python kernelspec to extend from.""" ksm = app.kernel_spec_manager - try_spec_names = getattr(settings, 'NOTEBOOK_KERNEL_SPEC_NAMES', [ - 'python3', - 'python', - ]) + try_spec_names = getattr( + settings, + "NOTEBOOK_KERNEL_SPEC_NAMES", + [ + "python3", + "python", + ], + ) if isinstance(try_spec_names, str): try_spec_names = [try_spec_names] @@ -187,7 +231,9 @@ def load_base_kernel_spec(self, app): except Exception: continue if not ks: - raise CommandError("No notebook (Python) kernel specs found. Tried %r" % try_spec_names) + raise CommandError( + "No notebook (Python) kernel specs found. Tried %r" % try_spec_names + ) return ks @@ -195,28 +241,30 @@ def generate_kernel_specs(self, app, ipython_arguments): """Generate an IPython >= 3.0 kernelspec that loads django extensions""" ks = self.load_base_kernel_spec(app) ks.argv.extend(ipython_arguments) - ks.display_name = getattr(settings, 'IPYTHON_KERNEL_DISPLAY_NAME', "Django Shell-Plus") + ks.display_name = getattr( + settings, "IPYTHON_KERNEL_DISPLAY_NAME", "Django Shell-Plus" + ) manage_py_dir, manage_py = os.path.split(os.path.realpath(sys.argv[0])) - if manage_py == 'manage.py' and os.path.isdir(manage_py_dir): - pythonpath = ks.env.get('PYTHONPATH', os.environ.get('PYTHONPATH', '')) + if manage_py == "manage.py" and os.path.isdir(manage_py_dir): + pythonpath = ks.env.get("PYTHONPATH", os.environ.get("PYTHONPATH", "")) pythonpath = pythonpath.split(os.pathsep) if manage_py_dir not in pythonpath: pythonpath.append(manage_py_dir) - ks.env['PYTHONPATH'] = os.pathsep.join(filter(None, pythonpath)) + ks.env["PYTHONPATH"] = os.pathsep.join(filter(None, pythonpath)) - return {'django_extensions': ks} + return {"django_extensions": ks} def run_notebookapp(self, app_init, options, use_kernel_specs=True, history=True): - no_browser = options['no_browser'] + no_browser = options["no_browser"] if self.extra_args: # if another '--' is found split the arguments notebook, ipython - if '--' in self.extra_args: - idx = self.extra_args.index('--') + if "--" in self.extra_args: + idx = self.extra_args.index("--") notebook_arguments = self.extra_args[:idx] - ipython_arguments = self.extra_args[idx + 1:] + ipython_arguments = self.extra_args[idx + 1 :] # otherwise pass the arguments to the notebook else: notebook_arguments = self.extra_args @@ -226,26 +274,35 @@ def run_notebookapp(self, app_init, options, use_kernel_specs=True, history=True ipython_arguments = self.get_ipython_arguments(options) # Treat IPYTHON_ARGUMENTS from settings - if 'django_extensions.management.notebook_extension' not in ipython_arguments: - ipython_arguments.extend(['--ext', 'django_extensions.management.notebook_extension']) + if "django_extensions.management.notebook_extension" not in ipython_arguments: + ipython_arguments.extend( + ["--ext", "django_extensions.management.notebook_extension"] + ) # Treat NOTEBOOK_ARGUMENTS from settings - if no_browser and '--no-browser' not in notebook_arguments: - notebook_arguments.append('--no-browser') - if '--notebook-dir' not in notebook_arguments and not any(e.startswith('--notebook-dir=') for e in notebook_arguments): - notebook_arguments.extend(['--notebook-dir', '.']) + if no_browser and "--no-browser" not in notebook_arguments: + notebook_arguments.append("--no-browser") + if "--notebook-dir" not in notebook_arguments and not any( + e.startswith("--notebook-dir=") for e in notebook_arguments + ): + notebook_arguments.extend(["--notebook-dir", "."]) # IPython < 3 passes through kernel args from notebook CLI if not use_kernel_specs: notebook_arguments.extend(ipython_arguments) # disable history if not already configured in some other way - if not history and not any(arg.startswith('--HistoryManager') for arg in ipython_arguments): - ipython_arguments.append('--HistoryManager.enabled=False') + if not history and not any( + arg.startswith("--HistoryManager") for arg in ipython_arguments + ): + ipython_arguments.append("--HistoryManager.enabled=False") if not callable(app_init): app = app_init - warnings.warn('Initialize should be a callable not an app instance', DeprecationWarning) + warnings.warn( + "Initialize should be a callable not an app instance", + DeprecationWarning, + ) app.initialize(notebook_arguments) else: app = app_init(notebook_arguments) @@ -255,27 +312,25 @@ def run_notebookapp(self, app_init, options, use_kernel_specs=True, history=True ksm = app.kernel_spec_manager for kid, ks in self.generate_kernel_specs(app, ipython_arguments).items(): roots = [os.path.dirname(ks.resource_dir), ksm.user_kernel_dir] - success = False + for root in roots: kernel_dir = os.path.join(root, kid) try: if not os.path.exists(kernel_dir): os.makedirs(kernel_dir) - - with open(os.path.join(kernel_dir, 'kernel.json'), 'w') as f: + with open(os.path.join(kernel_dir, "kernel.json"), "w") as f: f.write(ks.to_json()) - - success = True break except OSError: continue - - if not success: - raise CommandError("Could not write kernel %r in directories %r" % (kid, roots)) + else: + raise CommandError( + "Could not write kernel %r in directories %r" % (kid, roots) + ) app.start() - @shell_runner(flags=['--notebook'], name='IPython Notebook') + @shell_runner(flags=["--notebook"], name="IPython Notebook") def get_notebook(self, options): try: from IPython import release @@ -293,6 +348,7 @@ def get_notebook(self, options): return traceback.format_exc() try: from IPython.frontend.html.notebook import notebookapp + NotebookApp = notebookapp.NotebookApp except ImportError: return traceback.format_exc() @@ -306,9 +362,10 @@ def app_init(*args, **kwargs): def run_notebook(): self.run_notebookapp(app_init, options, use_kernel_specs) + return run_notebook - @shell_runner(flags=['--lab'], name='JupyterLab Notebook') + @shell_runner(flags=["--lab"], name="JupyterLab Notebook") def get_jupyterlab(self, options): try: from jupyterlab.labapp import LabApp @@ -324,6 +381,7 @@ def get_jupyterlab(self, options): if not NotebookApp or not issubclass(LabApp, NotebookApp): app_init = LabApp.initialize_server else: + def app_init(*args, **kwargs): app = LabApp.instance() app.initialize(*args, **kwargs) @@ -331,38 +389,26 @@ def app_init(*args, **kwargs): def run_jupyterlab(): self.run_notebookapp(app_init, options, history=False) + return run_jupyterlab - @shell_runner(flags=['--plain'], name='plain Python') + @shell_runner(flags=["--plain"], name="plain Python") def get_plain(self, options): # Using normal Python shell import code + + # Set up a dictionary to serve as the environment for the shell. imported_objects = self.get_imported_objects(options) - try: - # Try activating rlcompleter, because it's handy. - import readline - except ImportError: - pass - else: - # We don't have to wrap the following import in a 'try', because - # we already know 'readline' was imported successfully. - import rlcompleter - readline.set_completer(rlcompleter.Completer(imported_objects).complete) - # Enable tab completion on systems using libedit (e.g. macOS). - # These lines are copied from Lib/site.py on Python 3.4. - readline_doc = getattr(readline, '__doc__', '') - if readline_doc is not None and 'libedit' in readline_doc: - readline.parse_and_bind("bind ^I rl_complete") - else: - readline.parse_and_bind("tab:complete") - use_pythonrc = options['use_pythonrc'] - no_startup = options['no_startup'] + use_pythonrc = options["use_pythonrc"] + no_startup = options["no_startup"] # We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system # conventions and get $PYTHONSTARTUP first then .pythonrc.py. if use_pythonrc or not no_startup: - for pythonrc in OrderedSet([os.environ.get("PYTHONSTARTUP"), os.path.expanduser('~/.pythonrc.py')]): + for pythonrc in OrderedSet( + [os.environ.get("PYTHONSTARTUP"), os.path.expanduser("~/.pythonrc.py")] + ): if not pythonrc: continue if not os.path.isfile(pythonrc): @@ -372,17 +418,56 @@ def get_plain(self, options): # Match the behavior of the cpython shell where an error in # PYTHONSTARTUP prints an exception and continues. try: - exec(compile(pythonrc_code, pythonrc, 'exec'), imported_objects) + exec(compile(pythonrc_code, pythonrc, "exec"), imported_objects) except Exception: traceback.print_exc() if self.tests_mode: raise + # By default, this will set up readline to do tab completion and to read and + # write history to the .python_history file, but this can be overridden by + # $PYTHONSTARTUP or ~/.pythonrc.py. + try: + hook = sys.__interactivehook__ + except AttributeError: + # Match the behavior of the cpython shell where a missing + # sys.__interactivehook__ is ignored. + pass + else: + try: + hook() + except Exception: + # Match the behavior of the cpython shell where an error in + # sys.__interactivehook__ prints a warning and the exception + # and continues. + print("Failed calling sys.__interactivehook__") + traceback.print_exc() + + try: + # Try activating rlcompleter, because it's handy. + import readline + except ImportError: + pass + else: + # We don't have to wrap the following import in a 'try', because + # we already know 'readline' was imported successfully. + import rlcompleter + + readline.set_completer(rlcompleter.Completer(imported_objects).complete) + # Enable tab completion on systems using libedit (e.g. macOS). + # These lines are copied from Lib/site.py on Python 3.4. + readline_doc = getattr(readline, "__doc__", "") + if readline_doc is not None and "libedit" in readline_doc: + readline.parse_and_bind("bind ^I rl_complete") + else: + readline.parse_and_bind("tab:complete") + def run_plain(): code.interact(local=imported_objects) + return run_plain - @shell_runner(flags=['--bpython'], name='BPython') + @shell_runner(flags=["--bpython"], name="BPython") def get_bpython(self, options): try: from bpython import embed @@ -393,19 +478,23 @@ def run_bpython(): imported_objects = self.get_imported_objects(options) kwargs = {} if self.extra_args: - kwargs['args'] = self.extra_args + kwargs["args"] = self.extra_args embed(imported_objects, **kwargs) + return run_bpython - @shell_runner(flags=['--ipython'], name='IPython') + @shell_runner(flags=["--ipython"], name="IPython") def get_ipython(self, options): try: from IPython import start_ipython def run_ipython(): imported_objects = self.get_imported_objects(options) - ipython_arguments = self.extra_args or self.get_ipython_arguments(options) + ipython_arguments = self.extra_args or self.get_ipython_arguments( + options + ) start_ipython(argv=ipython_arguments, user_ns=imported_objects) + return run_ipython except ImportError: str_exc = traceback.format_exc() @@ -422,9 +511,10 @@ def run_ipython(): imported_objects = self.get_imported_objects(options) shell = IPShell(argv=[], user_ns=imported_objects) shell.mainloop() + return run_ipython - @shell_runner(flags=['--ptpython'], name='PTPython') + @shell_runner(flags=["--ptpython"], name="PTPython") def get_ptpython(self, options): try: from ptpython.repl import embed, run_config @@ -437,12 +527,17 @@ def get_ptpython(self, options): def run_ptpython(): imported_objects = self.get_imported_objects(options) - history_filename = os.path.expanduser('~/.ptpython_history') - embed(globals=imported_objects, history_filename=history_filename, - vi_mode=options['vi_mode'], configure=run_config) + history_filename = os.path.expanduser("~/.ptpython_history") + embed( + globals=imported_objects, + history_filename=history_filename, + vi_mode=options["vi_mode"], + configure=run_config, + ) + return run_ptpython - @shell_runner(flags=['--ptipython'], name='PT-IPython') + @shell_runner(flags=["--ptipython"], name="PT-IPython") def get_ptipython(self, options): try: from ptpython.repl import run_config @@ -457,19 +552,24 @@ def get_ptipython(self, options): def run_ptipython(): imported_objects = self.get_imported_objects(options) - history_filename = os.path.expanduser('~/.ptpython_history') - embed(user_ns=imported_objects, history_filename=history_filename, - vi_mode=options['vi_mode'], configure=run_config) + history_filename = os.path.expanduser("~/.ptpython_history") + embed( + user_ns=imported_objects, + history_filename=history_filename, + vi_mode=options["vi_mode"], + configure=run_config, + ) + return run_ptipython - @shell_runner(flags=['--idle'], name='Idle') + @shell_runner(flags=["--idle"], name="Idle") def get_idle(self, options): from idlelib.pyshell import main def run_idle(): sys.argv = [ sys.argv[0], - '-c', + "-c", """ from django_extensions.management import shells from django.core.management.color import no_style @@ -488,34 +588,41 @@ def set_application_name(self, options): Use the fallback_application_name to let the user override it with PGAPPNAME env variable - http://www.postgresql.org/docs/9.4/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS # noqa - """ + https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS + """ # noqa: E501 supported_backends = ( - 'django.db.backends.postgresql', - 'django.db.backends.postgresql_psycopg2', + "django.db.backends.postgresql", + "django.db.backends.postgresql_psycopg2", ) - opt_name = 'fallback_application_name' - default_app_name = 'django_shell' - dbs = getattr(settings, 'DATABASES', []) + opt_name = "fallback_application_name" + default_app_name = "django_shell" + dbs = getattr(settings, "DATABASES", []) for connection in connections.all(): alias = connection.alias mro = inspect.getmro(connection.__class__) if any(klass.__module__.startswith(supported_backends) for klass in mro): - if 'OPTIONS' not in dbs[alias] or opt_name not in dbs[alias]['OPTIONS']: - dbs[alias].setdefault('OPTIONS', {}).update({opt_name: default_app_name}) + if "OPTIONS" not in dbs[alias] or opt_name not in dbs[alias]["OPTIONS"]: + dbs[alias].setdefault("OPTIONS", {}).update( + {opt_name: default_app_name} + ) @signalcommand def handle(self, *args, **options): verbosity = options["verbosity"] - get_runner = options['runner'] - print_sql = getattr(settings, 'SHELL_PLUS_PRINT_SQL', False) + get_runner = options["runner"] + 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, truncate=truncate, print_sql_location=options["print_sql_location"], confprefix="SHELL_PLUS"): - SETTINGS_SHELL_PLUS = getattr(settings, 'SHELL_PLUS', None) + 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): for runner in self.runners: @@ -526,7 +633,7 @@ def get_runner_by_flag(flag): self.set_application_name(options) if not get_runner and SETTINGS_SHELL_PLUS: - get_runner = get_runner_by_flag('--%s' % SETTINGS_SHELL_PLUS) + get_runner = get_runner_by_flag("--%s" % SETTINGS_SHELL_PLUS) if not get_runner: runner = None runner_name = SETTINGS_SHELL_PLUS @@ -535,6 +642,7 @@ def get_runner_by_flag(flag): runner = get_runner(options) runner_name = get_runner.runner_name else: + def try_runner(get_runner): runner_name = get_runner.runner_name if verbosity > 2: @@ -550,9 +658,15 @@ def try_runner(get_runner): tried_runners = set() # try the runners that are least unexpected (normal shell runners) - preferred_runners = ['ptipython', 'ptpython', 'bpython', 'ipython', 'plain'] + preferred_runners = [ + "ptipython", + "ptpython", + "bpython", + "ipython", + "plain", + ] for flag_suffix in preferred_runners: - get_runner = get_runner_by_flag('--%s' % flag_suffix) + get_runner = get_runner_by_flag("--%s" % flag_suffix) tried_runners.add(get_runner) runner = try_runner(get_runner) if runner: @@ -578,9 +692,9 @@ def try_runner(get_runner): if self.tests_mode: return 130 - if options['command']: + if options["command"]: imported_objects = self.get_imported_objects(options) - exec(options['command'], imported_objects) + exec(options["command"], imported_objects) return None runner() diff --git a/django_extensions/management/commands/show_permissions.py b/django_extensions/management/commands/show_permissions.py new file mode 100644 index 000000000..2df0182f8 --- /dev/null +++ b/django_extensions/management/commands/show_permissions.py @@ -0,0 +1,60 @@ +from django.contrib.contenttypes.models import ContentType +from django.core.management.base import BaseCommand, CommandError + + +class Command(BaseCommand): + help = ( + "List all permissions for models. " + "By default, excludes admin, auth, contenttypes, and sessions apps." + ) + + def add_arguments(self, parser): + parser.add_argument( + "app_label_model", + nargs="*", + help="[app_label.]model(s) to show permissions for.", + ) + parser.add_argument( + "--all", + action="store_true", + help="Include results for admin, auth, contenttypes, and sessions.", + ) + parser.add_argument("--app-label", help="App label to dump permissions for.") + + def handle(self, *args, **options): + app_label_models = options["app_label_model"] + include_all = options["all"] + app_label_filter = options["app_label"] + + if include_all: + content_types = ContentType.objects.order_by("app_label", "model") + elif app_label_filter: + content_types = ContentType.objects.filter( + app_label=app_label_filter.lower() + ).order_by("app_label", "model") + if not content_types: + raise CommandError( + f'No content types found for app label "{app_label_filter}".' + ) + elif not app_label_models: + excluded = ["admin", "auth", "contenttypes", "sessions"] + content_types = ContentType.objects.exclude( + app_label__in=excluded + ).order_by("app_label", "model") + else: + content_types = [] + for value in app_label_models: + if "." in value: + app_label, model = value.split(".") + qs = ContentType.objects.filter(app_label=app_label, model=model) + else: + qs = ContentType.objects.filter(model=value) + + if not qs: + raise CommandError(f"Content type not found for '{value}'.") + content_types.extend(qs) + + for ct in content_types: + self.stdout.write(f"Permissions for {ct}") + for perm in ct.permission_set.all(): + self.stdout.write(f" {ct.app_label}.{perm.codename} | {perm.name}") diff --git a/django_extensions/management/commands/show_template_tags.py b/django_extensions/management/commands/show_template_tags.py index 79561890e..94ccd6394 100644 --- a/django_extensions/management/commands/show_template_tags.py +++ b/django_extensions/management/commands/show_template_tags.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import inspect import os import re @@ -8,7 +7,7 @@ from django.core.management import color from django.core.management import BaseCommand from django.utils import termcolors -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str from django_extensions.compat import load_tag_library from django_extensions.management.color import _dummy_style_func @@ -17,17 +16,17 @@ def no_style(): style = color.no_style() - for role in ('FILTER', 'MODULE_NAME', 'TAG', 'TAGLIB'): + for role in ("FILTER", "MODULE_NAME", "TAG", "TAGLIB"): setattr(style, role, _dummy_style_func) return style def color_style(): style = color.color_style() - style.FILTER = termcolors.make_style(fg='yellow', opts=('bold',)) - style.MODULE_NAME = termcolors.make_style(fg='green', opts=('bold',)) - style.TAG = termcolors.make_style(fg='red', opts=('bold',)) - style.TAGLIB = termcolors.make_style(fg='blue', opts=('bold',)) + style.FILTER = termcolors.make_style(fg="yellow", opts=("bold",)) + style.MODULE_NAME = termcolors.make_style(fg="green", opts=("bold",)) + style.TAG = termcolors.make_style(fg="red", opts=("bold",)) + style.TAGLIB = termcolors.make_style(fg="blue", opts=("bold",)) return style @@ -38,10 +37,10 @@ def format_block(block, nlspaces=0): The purpose is to let us list a code block as a multiline, triple-quoted Python string, taking care of indentation concerns. - http://code.activestate.com/recipes/145672/ + https://code.activestate.com/recipes/145672/ """ # separate block into lines - lines = smart_text(block).split('\n') + lines = smart_str(block).split("\n") # remove leading/trailing empty lines while lines and not lines[0]: @@ -50,9 +49,9 @@ def format_block(block, nlspaces=0): del lines[-1] # look at first line to see how much indentation to trim - ws = re.match(r'\s*', lines[0]).group(0) + ws = re.match(r"\s*", lines[0]).group(0) if ws: - lines = map(lambda x: x.replace(ws, '', 1), lines) + lines = map(lambda x: x.replace(ws, "", 1), lines) # remove leading/trailing blank lines (after leading ws removal) # we do this again in case there were pure-whitespace lines @@ -62,9 +61,9 @@ def format_block(block, nlspaces=0): del lines[-1] # account for user-specified leading spaces - flines = ['%s%s' % (' ' * nlspaces, line) for line in lines] + flines = ["%s%s" % (" " * nlspaces, line) for line in lines] - return '\n'.join(flines) + '\n' + return "\n".join(flines) + "\n" class Command(BaseCommand): @@ -72,11 +71,11 @@ class Command(BaseCommand): results = "" def add_result(self, s, depth=0): - self.results += '%s\n' % s.rjust(depth * 4 + len(s)) + self.results += "%s\n" % s.rjust(depth * 4 + len(s)) @signalcommand def handle(self, *args, **options): - if options['no_color']: + if options["no_color"]: style = no_style() else: style = color_style() @@ -84,12 +83,14 @@ def handle(self, *args, **options): for app_config in apps.get_app_configs(): app = app_config.name try: - templatetag_mod = __import__(app + '.templatetags', {}, {}, ['']) + templatetag_mod = __import__(app + ".templatetags", {}, {}, [""]) except ImportError: continue mod_path = inspect.getabsfile(templatetag_mod) mod_files = os.listdir(os.path.dirname(mod_path)) - tag_files = [i.rstrip('.py') for i in mod_files if i.endswith('.py') and i[0] != '_'] + tag_files = [ + i.rstrip(".py") for i in mod_files if i.endswith(".py") and i[0] != "_" + ] app_labeled = False for taglib in tag_files: lib = load_tag_library(taglib) @@ -97,16 +98,16 @@ def handle(self, *args, **options): continue if not app_labeled: - self.add_result('App: %s' % style.MODULE_NAME(app)) + self.add_result("App: %s" % style.MODULE_NAME(app)) app_labeled = True - self.add_result('load: %s' % style.TAGLIB(taglib), 1) + self.add_result("load: %s" % style.TAGLIB(taglib), 1) libstuff = [ - (lib.tags, 'Tag:', style.TAG), - (lib.filters, 'Filter:', style.FILTER) + (lib.tags, "Tag:", style.TAG), + (lib.filters, "Filter:", style.FILTER), ] for items, label, style_func in libstuff: for item in items: - self.add_result('%s %s' % (label, style_func(item)), 2) + self.add_result("%s %s" % (label, style_func(item)), 2) doc = inspect.getdoc(items[item]) if doc: self.add_result(format_block(doc, 12)) diff --git a/django_extensions/management/commands/show_urls.py b/django_extensions/management/commands/show_urls.py index cddca120e..c61739b83 100644 --- a/django_extensions/management/commands/show_urls.py +++ b/django_extensions/management/commands/show_urls.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import functools import json import re @@ -33,12 +31,12 @@ def describe_pattern(p): FMTR = { - 'dense': "{url}\t{module}\t{url_name}\t{decorator}", - 'table': "{url},{module},{url_name},{decorator}", - 'aligned': "{url},{module},{url_name},{decorator}", - 'verbose': "{url}\n\tController: {module}\n\tURL Name: {url_name}\n\tDecorators: {decorator}\n", - 'json': '', - 'pretty-json': '' + "dense": "{url}\t{module}\t{url_name}\t{decorator}", + "table": "{url},{module},{url_name},{decorator}", + "aligned": "{url},{module},{url_name},{decorator}", + "verbose": "{url}\n\tController: {module}\n\tURL Name: {url_name}\n\tDecorators: {decorator}\n", # noqa: E501 + "json": "", + "pretty-json": "", } @@ -48,73 +46,101 @@ class Command(BaseCommand): def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - "--unsorted", "-u", action="store_true", dest="unsorted", - help="Show urls unsorted but same order as found in url patterns" + "--unsorted", + "-u", + action="store_true", + dest="unsorted", + help="Show urls unsorted but same order as found in url patterns", ) parser.add_argument( - "--language", "-l", dest="language", - help="Only show this language code (useful for i18n_patterns)" + "--language", + "-l", + dest="language", + help="Only show this language code (useful for i18n_patterns)", ) parser.add_argument( - "--decorator", "-d", action="append", dest="decorator", default=[], - help="Show the presence of given decorator on views" + "--decorator", + "-d", + action="append", + dest="decorator", + default=[], + help="Show the presence of given decorator on views", ) parser.add_argument( - "--format", "-f", dest="format_style", default="dense", - help="Style of the output. Choices: %s" % FMTR.keys() + "--format", + "-f", + dest="format_style", + default="dense", + help="Style of the output. Choices: %s" % FMTR.keys(), ) parser.add_argument( - "--urlconf", "-c", dest="urlconf", default="ROOT_URLCONF", - help="Set the settings URL conf variable to use" + "--urlconf", + "-c", + dest="urlconf", + default="ROOT_URLCONF", + help="Set the settings URL conf variable to use", ) @signalcommand def handle(self, *args, **options): - style = no_style() if options['no_color'] else color_style() + style = no_style() if options["no_color"] else color_style() - language = options['language'] + language = options["language"] if language is not None: translation.activate(language) - self.LANGUAGES = [(code, name) for code, name in getattr(settings, 'LANGUAGES', []) if code == language] + self.LANGUAGES = [ + (code, name) + for code, name in getattr(settings, "LANGUAGES", []) + if code == language + ] else: - self.LANGUAGES = getattr(settings, 'LANGUAGES', ((None, None), )) + self.LANGUAGES = getattr(settings, "LANGUAGES", ((None, None),)) - decorator = options['decorator'] + decorator = options["decorator"] if not decorator: - decorator = ['login_required'] + decorator = ["login_required"] - format_style = options['format_style'] + format_style = options["format_style"] if format_style not in FMTR: raise CommandError( - "Format style '%s' does not exist. Options: %s" % ( + "Format style '%s' does not exist. Options: %s" + % ( format_style, ", ".join(sorted(FMTR.keys())), ) ) - pretty_json = format_style == 'pretty-json' + pretty_json = format_style == "pretty-json" if pretty_json: - format_style = 'json' + format_style = "json" fmtr = FMTR[format_style] - urlconf = options['urlconf'] + urlconf = options["urlconf"] views = [] if not hasattr(settings, urlconf): - raise CommandError("Settings module {} does not have the attribute {}.".format(settings, urlconf)) + raise CommandError( + "Settings module {} does not have the attribute {}.".format( + settings, urlconf + ) + ) try: - urlconf = __import__(getattr(settings, urlconf), {}, {}, ['']) + urlconf = __import__(getattr(settings, urlconf), {}, {}, [""]) except Exception as e: - if options['traceback']: + if options["traceback"]: import traceback + traceback.print_exc() - raise CommandError("Error occurred while trying to load %s: %s" % (getattr(settings, urlconf), str(e))) + raise CommandError( + "Error occurred while trying to load %s: %s" + % (getattr(settings, urlconf), str(e)) + ) view_functions = self.extract_views_from_urlpatterns(urlconf.urlpatterns) - for (func, regex, url_name) in view_functions: - if hasattr(func, '__globals__'): + for func, regex, url_name in view_functions: + if hasattr(func, "__globals__"): func_globals = func.__globals__ - elif hasattr(func, 'func_globals'): + elif hasattr(func, "func_globals"): func_globals = func.func_globals else: func_globals = {} @@ -123,69 +149,95 @@ def handle(self, *args, **options): if isinstance(func, functools.partial): func = func.func - decorators.insert(0, 'functools.partial') + decorators.insert(0, "functools.partial") - if hasattr(func, '__name__'): + if hasattr(func, "view_class"): + func = func.view_class + if hasattr(func, "__name__"): func_name = func.__name__ - elif hasattr(func, '__class__'): - func_name = '%s()' % func.__class__.__name__ + elif hasattr(func, "__class__"): + func_name = "%s()" % func.__class__.__name__ else: - func_name = re.sub(r' at 0x[0-9a-f]+', '', repr(func)) + func_name = re.sub(r" at 0x[0-9a-f]+", "", repr(func)) - module = '{0}.{1}'.format(func.__module__, func_name) - url_name = url_name or '' + module = "{0}.{1}".format(func.__module__, func_name) + url_name = url_name or "" url = simplify_regex(regex) - decorator = ', '.join(decorators) - - if format_style == 'json': - views.append({"url": url, "module": module, "name": url_name, "decorators": decorator}) + decorator = ", ".join(decorators) + + if format_style == "json": + views.append( + { + "url": url, + "module": module, + "name": url_name, + "decorators": decorator, + } + ) else: - views.append(fmtr.format( - module='{0}.{1}'.format(style.MODULE(func.__module__), style.MODULE_NAME(func_name)), - url_name=style.URL_NAME(url_name), - url=style.URL(url), - decorator=decorator, - ).strip()) - - if not options['unsorted'] and format_style != 'json': + views.append( + fmtr.format( + module="{0}.{1}".format( + style.MODULE(func.__module__), style.MODULE_NAME(func_name) + ), + url_name=style.URL_NAME(url_name), + url=style.URL(url), + decorator=decorator, + ).strip() + ) + + if not options["unsorted"] and format_style != "json": views = sorted(views) - if format_style == 'aligned': - views = [row.split(',', 3) for row in views] + if format_style == "aligned": + views = [row.split(",", 3) for row in views] widths = [len(max(columns, key=len)) for columns in zip(*views)] views = [ - ' '.join('{0:<{1}}'.format(cdata, width) for width, cdata in zip(widths, row)) + " ".join( + "{0:<{1}}".format(cdata, width) for width, cdata in zip(widths, row) + ) for row in views ] - elif format_style == 'table': + elif format_style == "table": # Reformat all data and show in a table format - views = [row.split(',', 3) for row in views] + views = [row.split(",", 3) for row in views] widths = [len(max(columns, key=len)) for columns in zip(*views)] table_views = [] - header = (style.MODULE_NAME('URL'), style.MODULE_NAME('Module'), style.MODULE_NAME('Name'), style.MODULE_NAME('Decorator')) + header = ( + style.MODULE_NAME("URL"), + style.MODULE_NAME("Module"), + style.MODULE_NAME("Name"), + style.MODULE_NAME("Decorator"), + ) table_views.append( - ' | '.join('{0:<{1}}'.format(title, width) for width, title in zip(widths, header)) + " | ".join( + "{0:<{1}}".format(title, width) + for width, title in zip(widths, header) + ) ) - table_views.append('-+-'.join('-' * width for width in widths)) + table_views.append("-+-".join("-" * width for width in widths)) for row in views: table_views.append( - ' | '.join('{0:<{1}}'.format(cdata, width) for width, cdata in zip(widths, row)) + " | ".join( + "{0:<{1}}".format(cdata, width) + for width, cdata in zip(widths, row) + ) ) # Replace original views so we can return the same object views = table_views - elif format_style == 'json': + elif format_style == "json": if pretty_json: return json.dumps(views, indent=4) return json.dumps(views) return "\n".join([v for v in views]) + "\n" - def extract_views_from_urlpatterns(self, urlpatterns, base='', namespace=None): + def extract_views_from_urlpatterns(self, urlpatterns, base="", namespace=None): """ Return a list of views from a list of urlpatterns. @@ -198,7 +250,7 @@ def extract_views_from_urlpatterns(self, urlpatterns, base='', namespace=None): if not p.name: name = p.name elif namespace: - name = '{0}:{1}'.format(namespace, p.name) + name = "{0}:{1}".format(namespace, p.name) else: name = p.name pattern = describe_pattern(p) @@ -211,27 +263,41 @@ def extract_views_from_urlpatterns(self, urlpatterns, base='', namespace=None): except ImportError: continue if namespace and p.namespace: - _namespace = '{0}:{1}'.format(namespace, p.namespace) + _namespace = "{0}:{1}".format(namespace, p.namespace) else: - _namespace = (p.namespace or namespace) + _namespace = p.namespace or namespace pattern = describe_pattern(p) if isinstance(p, LocaleRegexURLResolver): for language in self.LANGUAGES: with translation.override(language[0]): - views.extend(self.extract_views_from_urlpatterns(patterns, base + pattern, namespace=_namespace)) + views.extend( + self.extract_views_from_urlpatterns( + patterns, base + pattern, namespace=_namespace + ) + ) else: - views.extend(self.extract_views_from_urlpatterns(patterns, base + pattern, namespace=_namespace)) - elif hasattr(p, '_get_callback'): + views.extend( + self.extract_views_from_urlpatterns( + patterns, base + pattern, namespace=_namespace + ) + ) + elif hasattr(p, "_get_callback"): try: - views.append((p._get_callback(), base + describe_pattern(p), p.name)) + views.append( + (p._get_callback(), base + describe_pattern(p), p.name) + ) except ViewDoesNotExist: continue - elif hasattr(p, 'url_patterns') or hasattr(p, '_get_url_patterns'): + elif hasattr(p, "url_patterns") or hasattr(p, "_get_url_patterns"): try: patterns = p.url_patterns except ImportError: continue - views.extend(self.extract_views_from_urlpatterns(patterns, base + describe_pattern(p), namespace=namespace)) + views.extend( + self.extract_views_from_urlpatterns( + patterns, base + describe_pattern(p), namespace=namespace + ) + ) else: raise TypeError("%s does not appear to be a urlpattern object" % p) return views diff --git a/django_extensions/management/commands/sqlcreate.py b/django_extensions/management/commands/sqlcreate.py index a0af17549..62f3c4621 100644 --- a/django_extensions/management/commands/sqlcreate.py +++ b/django_extensions/management/commands/sqlcreate.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- import socket import sys import warnings +from typing import List from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -17,77 +17,108 @@ class Command(BaseCommand): The envisioned use case is something like this: ./manage.py sqlcreate [--database=] | mysql -u -p - ./manage.py sqlcreate [--database=] | psql -U -W""" + ./manage.py sqlcreate [--database=] | psql -U -W + """ # noqa: E501 - requires_system_checks = False + requires_system_checks: List[str] = [] can_import_settings = True def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '-R', '--router', action='store', dest='router', default=DEFAULT_DB_ALIAS, - help='Use this router-database other then defined in settings.py' + "-R", + "--router", + action="store", + dest="router", + default=DEFAULT_DB_ALIAS, + help="Use this router-database other then defined in settings.py", ) parser.add_argument( - '--database', default=DEFAULT_DB_ALIAS, - help='Nominates a database to run command for. Defaults to the "%s" database.' % DEFAULT_DB_ALIAS, + "--database", + default=DEFAULT_DB_ALIAS, + help=( + "Nominates a database to run command for. " + 'Defaults to the "%s" database.' + ) + % DEFAULT_DB_ALIAS, ) parser.add_argument( - '-D', '--drop', action='store_true', dest='drop', default=False, - help='If given, includes commands to drop any existing user and database.' + "-D", + "--drop", + action="store_true", + dest="drop", + default=False, + help="If given, includes commands to drop any existing user and database.", ) @signalcommand def handle(self, *args, **options): - database = options['database'] - if options['router'] != DEFAULT_DB_ALIAS: - warnings.warn("--router is deprecated. You should use --database.", RemovedInNextVersionWarning, stacklevel=2) - database = options['router'] + database = options["database"] + if options["router"] != DEFAULT_DB_ALIAS: + warnings.warn( + "--router is deprecated. You should use --database.", + RemovedInNextVersionWarning, + stacklevel=2, + ) + database = options["router"] dbinfo = settings.DATABASES.get(database) if dbinfo is None: raise CommandError("Unknown database %s" % database) - engine = dbinfo.get('ENGINE') - dbuser = dbinfo.get('USER') - dbpass = dbinfo.get('PASSWORD') - dbname = dbinfo.get('NAME') - dbhost = dbinfo.get('HOST') + engine = dbinfo.get("ENGINE") + dbuser = dbinfo.get("USER") + dbpass = dbinfo.get("PASSWORD") + dbname = dbinfo.get("NAME") + dbhost = dbinfo.get("HOST") dbclient = socket.gethostname() # django settings file tells you that localhost should be specified by leaving # the DATABASE_HOST blank if not dbhost: - dbhost = 'localhost' + dbhost = "localhost" if engine in SQLITE_ENGINES: - sys.stderr.write("-- manage.py migrate will automatically create a sqlite3 database file.\n") + sys.stderr.write( + "-- manage.py migrate will automatically create a sqlite3 database file.\n" # noqa: E501 + ) elif engine in MYSQL_ENGINES: - sys.stderr.write("""-- WARNING!: https://docs.djangoproject.com/en/dev/ref/databases/#collation-settings + sys.stderr.write( + """-- WARNING!: https://docs.djangoproject.com/en/dev/ref/databases/#collation-settings -- Please read this carefully! Collation will be set to utf8_bin to have case-sensitive data. -""") +""" # noqa: E501 + ) print("CREATE DATABASE %s CHARACTER SET utf8 COLLATE utf8_bin;" % dbname) - print("GRANT ALL PRIVILEGES ON %s.* to '%s'@'%s' identified by '%s';" % ( - dbname, dbuser, dbclient, dbpass - )) + print( + "GRANT ALL PRIVILEGES ON %s.* to '%s'@'%s' identified by '%s';" + % (dbname, dbuser, dbclient, dbpass) + ) elif engine in POSTGRESQL_ENGINES: - if options['drop']: + if options["drop"]: print("DROP DATABASE IF EXISTS %s;" % (dbname,)) if dbuser: print("DROP USER IF EXISTS %s;" % (dbuser,)) if dbuser and dbpass: - print("CREATE USER %s WITH ENCRYPTED PASSWORD '%s' CREATEDB;" % (dbuser, dbpass)) - print("CREATE DATABASE %s WITH ENCODING 'UTF-8' OWNER \"%s\";" % (dbname, dbuser)) + print( + "CREATE USER %s WITH ENCRYPTED PASSWORD '%s' CREATEDB;" + % (dbuser, dbpass) + ) + print( + "CREATE DATABASE %s WITH ENCODING 'UTF-8' OWNER \"%s\";" + % (dbname, dbuser) + ) print("GRANT ALL PRIVILEGES ON DATABASE %s TO %s;" % (dbname, dbuser)) else: print( - "-- Assuming that unix domain socket connection mode is being used because\n" + "-- Assuming that unix domain socket connection mode is being used because\n" # noqa: E501 "-- USER or PASSWORD are blank in Django DATABASES configuration." ) - print("CREATE DATABASE %s WITH ENCODING 'UTF-8';" % (dbname, )) + print("CREATE DATABASE %s WITH ENCODING 'UTF-8';" % (dbname,)) else: # CREATE DATABASE is not SQL standard, but seems to be supported by most. - sys.stderr.write("-- Don't know how to handle '%s' falling back to SQL.\n" % engine) + sys.stderr.write( + "-- Don't know how to handle '%s' falling back to SQL.\n" % engine + ) print("CREATE DATABASE %s;" % dbname) print("GRANT ALL PRIVILEGES ON DATABASE %s to %s;" % (dbname, dbuser)) diff --git a/django_extensions/management/commands/sqldiff.py b/django_extensions/management/commands/sqldiff.py index 550ed8f7b..58a40ca8a 100644 --- a/django_extensions/management/commands/sqldiff.py +++ b/django_extensions/management/commands/sqldiff.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ sqldiff.py - Prints the (approximated) difference between models and database @@ -30,12 +29,13 @@ 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 from django_extensions.management.utils import signalcommand -ORDERING_FIELD = IntegerField('_order', null=True) +ORDERING_FIELD: IntegerField = IntegerField("_order", null=True) def flatten(lst, ltypes=(list, tuple)): @@ -49,7 +49,7 @@ def flatten(lst, ltypes=(list, tuple)): i -= 1 break else: - lst[i:i + 1] = lst[i] + lst[i : i + 1] = lst[i] i += 1 return ltype(lst) @@ -76,117 +76,138 @@ class SQLDiff: ] DIFF_TYPES = [ - 'error', - 'comment', - 'table-missing-in-db', - 'table-missing-in-model', - 'field-missing-in-db', - 'field-missing-in-model', - 'fkey-missing-in-db', - 'fkey-missing-in-model', - 'index-missing-in-db', - 'index-missing-in-model', - 'unique-missing-in-db', - 'unique-missing-in-model', - 'field-type-differ', - 'field-parameter-differ', - 'notnull-differ', + "error", + "comment", + "table-missing-in-db", + "table-missing-in-model", + "field-missing-in-db", + "field-missing-in-model", + "fkey-missing-in-db", + "fkey-missing-in-model", + "index-missing-in-db", + "index-missing-in-model", + "unique-missing-in-db", + "unique-missing-in-model", + "field-type-differ", + "field-parameter-differ", + "notnull-differ", ] DIFF_TEXTS = { - 'error': 'error: %(0)s', - 'comment': 'comment: %(0)s', - 'table-missing-in-db': "table '%(0)s' missing in database", - 'table-missing-in-model': "table '%(0)s' missing in models", - 'field-missing-in-db': "field '%(1)s' defined in model but missing in database", - 'field-missing-in-model': "field '%(1)s' defined in database but missing in model", - 'fkey-missing-in-db': "field '%(1)s' FOREIGN KEY defined in model but missing in database", - 'fkey-missing-in-model': "field '%(1)s' FOREIGN KEY defined in database but missing in model", - 'index-missing-in-db': "field '%(1)s' INDEX named '%(2)s' defined in model but missing in database", - 'index-missing-in-model': "field '%(1)s' INDEX defined in database schema but missing in model", - 'unique-missing-in-db': "field '%(1)s' UNIQUE named '%(2)s' defined in model but missing in database", - 'unique-missing-in-model': "field '%(1)s' UNIQUE defined in database schema but missing in model", - 'field-type-differ': "field '%(1)s' not of same type: db='%(3)s', model='%(2)s'", - 'field-parameter-differ': "field '%(1)s' parameters differ: db='%(3)s', model='%(2)s'", - 'notnull-differ': "field '%(1)s' null constraint should be '%(2)s' in the database", + "error": "error: %(0)s", + "comment": "comment: %(0)s", + "table-missing-in-db": "table '%(0)s' missing in database", + "table-missing-in-model": "table '%(0)s' missing in models", + "field-missing-in-db": "field '%(1)s' defined in model but missing in database", + "field-missing-in-model": "field '%(1)s' defined in database but missing in model", # noqa: E501 + "fkey-missing-in-db": "field '%(1)s' FOREIGN KEY defined in model but missing in database", # noqa: E501 + "fkey-missing-in-model": "field '%(1)s' FOREIGN KEY defined in database but missing in model", # noqa: E501 + "index-missing-in-db": "field '%(1)s' INDEX named '%(2)s' defined in model but missing in database", # noqa: E501 + "index-missing-in-model": "field '%(1)s' INDEX defined in database schema but missing in model", # noqa: E501 + "unique-missing-in-db": "field '%(1)s' UNIQUE named '%(2)s' defined in model but missing in database", # noqa: E501 + "unique-missing-in-model": "field '%(1)s' UNIQUE defined in database schema but missing in model", # noqa: E501 + "field-type-differ": "field '%(1)s' not of same type: db='%(3)s', model='%(2)s'", # noqa: E501 + "field-parameter-differ": "field '%(1)s' parameters differ: db='%(3)s', model='%(2)s'", # noqa: E501 + "notnull-differ": "field '%(1)s' null constraint should be '%(2)s' in the database", # noqa: E501 } SQL_FIELD_MISSING_IN_DB = lambda self, style, qn, args: "%s %s\n\t%s %s %s;" % ( - style.SQL_KEYWORD('ALTER TABLE'), + style.SQL_KEYWORD("ALTER TABLE"), style.SQL_TABLE(qn(args[0])), - style.SQL_KEYWORD('ADD COLUMN'), + style.SQL_KEYWORD("ADD COLUMN"), style.SQL_FIELD(qn(args[1])), - ' '.join(style.SQL_COLTYPE(a) if i == 0 else style.SQL_KEYWORD(a) for i, a in enumerate(args[2:])) + " ".join( + style.SQL_COLTYPE(a) if i == 0 else style.SQL_KEYWORD(a) + for i, a in enumerate(args[2:]) + ), ) SQL_FIELD_MISSING_IN_MODEL = lambda self, style, qn, args: "%s %s\n\t%s %s;" % ( - style.SQL_KEYWORD('ALTER TABLE'), + style.SQL_KEYWORD("ALTER TABLE"), style.SQL_TABLE(qn(args[0])), - style.SQL_KEYWORD('DROP COLUMN'), - style.SQL_FIELD(qn(args[1])) - ) - SQL_FKEY_MISSING_IN_DB = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s %s (%s)%s;" % ( - style.SQL_KEYWORD('ALTER TABLE'), - style.SQL_TABLE(qn(args[0])), - style.SQL_KEYWORD('ADD COLUMN'), + style.SQL_KEYWORD("DROP COLUMN"), style.SQL_FIELD(qn(args[1])), - ' '.join(style.SQL_COLTYPE(a) if i == 0 else style.SQL_KEYWORD(a) for i, a in enumerate(args[4:])), - style.SQL_KEYWORD('REFERENCES'), - style.SQL_TABLE(qn(args[2])), - style.SQL_FIELD(qn(args[3])), - connection.ops.deferrable_sql() + ) + SQL_FKEY_MISSING_IN_DB = ( + lambda self, style, qn, args: "%s %s\n\t%s %s %s %s %s (%s)%s;" + % ( + style.SQL_KEYWORD("ALTER TABLE"), + style.SQL_TABLE(qn(args[0])), + style.SQL_KEYWORD("ADD COLUMN"), + style.SQL_FIELD(qn(args[1])), + " ".join( + style.SQL_COLTYPE(a) if i == 0 else style.SQL_KEYWORD(a) + for i, a in enumerate(args[4:]) + ), + style.SQL_KEYWORD("REFERENCES"), + style.SQL_TABLE(qn(args[2])), + style.SQL_FIELD(qn(args[3])), + connection.ops.deferrable_sql(), + ) ) SQL_INDEX_MISSING_IN_DB = lambda self, style, qn, args: "%s %s\n\t%s %s (%s%s);" % ( - style.SQL_KEYWORD('CREATE INDEX'), + style.SQL_KEYWORD("CREATE INDEX"), style.SQL_TABLE(qn(args[2])), - # style.SQL_TABLE(qn("%s" % '_'.join('_'.join(a) if isinstance(a, (list, tuple)) else a for a in args[0:3] if a))), - style.SQL_KEYWORD('ON'), style.SQL_TABLE(qn(args[0])), - style.SQL_FIELD(', '.join(qn(e) for e in args[1])), - style.SQL_KEYWORD(args[3]) + # style.SQL_TABLE(qn("%s" % '_'.join('_'.join(a) if isinstance(a, (list, tuple)) else a for a in args[0:3] if a))), # noqa: E501 + style.SQL_KEYWORD("ON"), + style.SQL_TABLE(qn(args[0])), + style.SQL_FIELD(", ".join(qn(e) for e in args[1])), + style.SQL_KEYWORD(args[3]), ) SQL_INDEX_MISSING_IN_MODEL = lambda self, style, qn, args: "%s %s;" % ( - style.SQL_KEYWORD('DROP INDEX'), - style.SQL_TABLE(qn(args[1])) + style.SQL_KEYWORD("DROP INDEX"), + style.SQL_TABLE(qn(args[1])), ) - SQL_UNIQUE_MISSING_IN_DB = lambda self, style, qn, args: "%s %s\n\t%s %s %s (%s);" % ( - style.SQL_KEYWORD('ALTER TABLE'), - style.SQL_TABLE(qn(args[0])), - style.SQL_KEYWORD('ADD CONSTRAINT'), - style.SQL_TABLE(qn(args[2])), - style.SQL_KEYWORD('UNIQUE'), - style.SQL_FIELD(', '.join(qn(e) for e in args[1])) + SQL_UNIQUE_MISSING_IN_DB = ( + lambda self, style, qn, args: "%s %s\n\t%s %s %s (%s);" + % ( + style.SQL_KEYWORD("ALTER TABLE"), + style.SQL_TABLE(qn(args[0])), + style.SQL_KEYWORD("ADD CONSTRAINT"), + style.SQL_TABLE(qn(args[2])), + style.SQL_KEYWORD("UNIQUE"), + style.SQL_FIELD(", ".join(qn(e) for e in args[1])), + ) ) SQL_UNIQUE_MISSING_IN_MODEL = lambda self, style, qn, args: "%s %s\n\t%s %s %s;" % ( - style.SQL_KEYWORD('ALTER TABLE'), + style.SQL_KEYWORD("ALTER TABLE"), style.SQL_TABLE(qn(args[0])), - style.SQL_KEYWORD('DROP'), - style.SQL_KEYWORD('CONSTRAINT'), - style.SQL_TABLE(qn(args[1])) + style.SQL_KEYWORD("DROP"), + style.SQL_KEYWORD("CONSTRAINT"), + style.SQL_TABLE(qn(args[1])), ) SQL_FIELD_TYPE_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s;" % ( - style.SQL_KEYWORD('ALTER TABLE'), + style.SQL_KEYWORD("ALTER TABLE"), style.SQL_TABLE(qn(args[0])), style.SQL_KEYWORD("MODIFY"), style.SQL_FIELD(qn(args[1])), - style.SQL_COLTYPE(args[2]) + style.SQL_COLTYPE(args[2]), ) SQL_FIELD_PARAMETER_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s;" % ( - style.SQL_KEYWORD('ALTER TABLE'), + style.SQL_KEYWORD("ALTER TABLE"), style.SQL_TABLE(qn(args[0])), style.SQL_KEYWORD("MODIFY"), style.SQL_FIELD(qn(args[1])), - style.SQL_COLTYPE(args[2]) + style.SQL_COLTYPE(args[2]), ) SQL_NOTNULL_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % ( - style.SQL_KEYWORD('ALTER TABLE'), + style.SQL_KEYWORD("ALTER TABLE"), style.SQL_TABLE(qn(args[0])), - style.SQL_KEYWORD('MODIFY'), + style.SQL_KEYWORD("MODIFY"), style.SQL_FIELD(qn(args[1])), style.SQL_KEYWORD(args[2]), - style.SQL_KEYWORD('NOT NULL') + style.SQL_KEYWORD("NOT NULL"), + ) + SQL_ERROR = lambda self, style, qn, args: style.NOTICE( + "-- Error: %s" % style.ERROR(args[0]) + ) + SQL_COMMENT = lambda self, style, qn, args: style.NOTICE( + "-- Comment: %s" % style.SQL_TABLE(args[0]) + ) + SQL_TABLE_MISSING_IN_DB = lambda self, style, qn, args: style.NOTICE( + "-- Table missing: %s" % args[0] + ) + SQL_TABLE_MISSING_IN_MODEL = lambda self, style, qn, args: style.NOTICE( + "-- Model missing for table: %s" % args[0] ) - SQL_ERROR = lambda self, style, qn, args: style.NOTICE('-- Error: %s' % style.ERROR(args[0])) - SQL_COMMENT = lambda self, style, qn, args: style.NOTICE('-- Comment: %s' % style.SQL_TABLE(args[0])) - SQL_TABLE_MISSING_IN_DB = lambda self, style, qn, args: style.NOTICE('-- Table missing: %s' % args[0]) - SQL_TABLE_MISSING_IN_MODEL = lambda self, style, qn, args: style.NOTICE('-- Model missing for table: %s' % args[0]) can_detect_notnull_differ = False can_detect_unsigned_differ = False @@ -196,7 +217,7 @@ def __init__(self, app_models, options, stdout, stderr): self.has_differences = None self.app_models = app_models self.options = options - self.dense = options['dense_output'] + self.dense = options["dense_output"] self.stdout = stdout self.stderr = stderr @@ -209,28 +230,33 @@ def __init__(self, app_models, options, stdout, stderr): self.unsigned = set() self.DIFF_SQL = { - 'error': self.SQL_ERROR, - 'comment': self.SQL_COMMENT, - 'table-missing-in-db': self.SQL_TABLE_MISSING_IN_DB, - 'table-missing-in-model': self.SQL_TABLE_MISSING_IN_MODEL, - 'field-missing-in-db': self.SQL_FIELD_MISSING_IN_DB, - 'field-missing-in-model': self.SQL_FIELD_MISSING_IN_MODEL, - 'fkey-missing-in-db': self.SQL_FKEY_MISSING_IN_DB, - 'fkey-missing-in-model': self.SQL_FIELD_MISSING_IN_MODEL, - 'index-missing-in-db': self.SQL_INDEX_MISSING_IN_DB, - 'index-missing-in-model': self.SQL_INDEX_MISSING_IN_MODEL, - 'unique-missing-in-db': self.SQL_UNIQUE_MISSING_IN_DB, - 'unique-missing-in-model': self.SQL_UNIQUE_MISSING_IN_MODEL, - 'field-type-differ': self.SQL_FIELD_TYPE_DIFFER, - 'field-parameter-differ': self.SQL_FIELD_PARAMETER_DIFFER, - 'notnull-differ': self.SQL_NOTNULL_DIFFER, + "error": self.SQL_ERROR, + "comment": self.SQL_COMMENT, + "table-missing-in-db": self.SQL_TABLE_MISSING_IN_DB, + "table-missing-in-model": self.SQL_TABLE_MISSING_IN_MODEL, + "field-missing-in-db": self.SQL_FIELD_MISSING_IN_DB, + "field-missing-in-model": self.SQL_FIELD_MISSING_IN_MODEL, + "fkey-missing-in-db": self.SQL_FKEY_MISSING_IN_DB, + "fkey-missing-in-model": self.SQL_FIELD_MISSING_IN_MODEL, + "index-missing-in-db": self.SQL_INDEX_MISSING_IN_DB, + "index-missing-in-model": self.SQL_INDEX_MISSING_IN_MODEL, + "unique-missing-in-db": self.SQL_UNIQUE_MISSING_IN_DB, + "unique-missing-in-model": self.SQL_UNIQUE_MISSING_IN_MODEL, + "field-type-differ": self.SQL_FIELD_TYPE_DIFFER, + "field-parameter-differ": self.SQL_FIELD_PARAMETER_DIFFER, + "notnull-differ": self.SQL_NOTNULL_DIFFER, } def load(self): self.cursor = connection.cursor() - self.django_tables = self.introspection.django_table_names(only_existing=self.options['only_existing']) + self.django_tables = self.introspection.django_table_names( + only_existing=self.options["only_existing"] + ) # TODO: We are losing information about tables which are views here - self.db_tables = [table_info.name for table_info in self.introspection.get_table_list(self.cursor)] + self.db_tables = [ + table_info.name + for table_info in self.introspection.get_table_list(self.cursor) + ] if self.can_detect_notnull_differ: self.load_null() @@ -239,16 +265,26 @@ def load(self): self.load_unsigned() def load_null(self): - raise NotImplementedError("load_null functions must be implemented if diff backend has 'can_detect_notnull_differ' set to True") + raise NotImplementedError( + ( + "load_null functions must be implemented if diff backend has " + "'can_detect_notnull_differ' set to True" + ) + ) def load_unsigned(self): - raise NotImplementedError("load_unsigned function must be implemented if diff backend has 'can_detect_unsigned_differ' set to True") + raise NotImplementedError( + ( + "load_unsigned function must be implemented if diff backend has " + "'can_detect_unsigned_differ' set to True" + ) + ) def add_app_model_marker(self, app_label, model_name): self.differences.append((app_label, model_name, [])) def add_difference(self, diff_type, *args): - assert diff_type in self.DIFF_TYPES, 'Unknown difference type' + assert diff_type in self.DIFF_TYPES, "Unknown difference type" self.differences[-1][-1].append((diff_type, args)) def get_data_types_reverse_override(self): @@ -264,7 +300,7 @@ def sql_to_dict(self, query, param): sql_to_dict(query, param) -> list of dicts - code from snippet at http://www.djangosnippets.org/snippets/1383/ + code from snippet at https://www.djangosnippets.org/snippets/1383/ """ cursor = connection.cursor() cursor.execute(query, param) @@ -281,12 +317,19 @@ def sql_to_dict(self, query, param): def get_field_model_type(self, field): return field.db_type(connection=connection) - def get_field_db_type_kwargs(self, current_kwargs, description, field=None, table_name=None, reverse_type=None): + def get_field_db_type_kwargs( + self, + current_kwargs, + description, + field=None, + table_name=None, + reverse_type=None, + ): return {} def get_field_db_type(self, description, field=None, table_name=None): # DB-API cursor.description - # (name, type_code, display_size, internal_size, precision, scale, null_ok) = description + # (name, type_code, display_size, internal_size, precision, scale, null_ok) type_code = description[1] DATA_TYPES_REVERSE_OVERRIDE = self.get_data_types_reverse_override() if type_code in DATA_TYPES_REVERSE_OVERRIDE: @@ -301,7 +344,11 @@ def get_field_db_type(self, description, field=None, table_name=None): key = (self.differences[-1][:2], description[:2]) if key not in self.unknown_db_fields: self.unknown_db_fields[key] = 1 - self.add_difference('comment', "Unknown database type for field '%s' (%s)" % (description[0], type_code)) + self.add_difference( + "comment", + "Unknown database type for field '%s' (%s)" + % (description[0], type_code), + ) return None if callable(reverse_type): @@ -310,41 +357,51 @@ def get_field_db_type(self, description, field=None, table_name=None): kwargs = {} if isinstance(reverse_type, dict): - kwargs.update(reverse_type['kwargs']) - reverse_type = reverse_type['name'] + kwargs.update(reverse_type["kwargs"]) + reverse_type = reverse_type["name"] - if type_code == 16946 and field and getattr(field, 'geom_type', None) == 'POINT': - reverse_type = 'django.contrib.gis.db.models.fields.PointField' + if ( + type_code == 16946 + and field + and getattr(field, "geom_type", None) == "POINT" + ): + reverse_type = "django.contrib.gis.db.models.fields.PointField" if isinstance(reverse_type, tuple): kwargs.update(reverse_type[1]) reverse_type = reverse_type[0] if reverse_type == "CharField" and description[3]: - kwargs['max_length'] = description[3] + kwargs["max_length"] = description[3] if reverse_type == "DecimalField": - kwargs['max_digits'] = description[4] - kwargs['decimal_places'] = description[5] and abs(description[5]) or description[5] + kwargs["max_digits"] = description[4] + kwargs["decimal_places"] = ( + description[5] and abs(description[5]) or description[5] + ) if description[6]: - kwargs['blank'] = True - if reverse_type not in ('TextField', 'CharField'): - kwargs['null'] = True + kwargs["blank"] = True + if reverse_type not in ("TextField", "CharField"): + kwargs["null"] = True - if field and getattr(field, 'geography', False): - kwargs['geography'] = True + if field and getattr(field, "geography", False): + kwargs["geography"] = True - if reverse_type == 'GeometryField': + if reverse_type == "GeometryField": geo_col = description[0] # Getting a more specific field type and any additional parameters # from the `get_geometry_type` routine for the spatial backend. - reverse_type, geo_params = self.introspection.get_geometry_type(table_name, geo_col) + reverse_type, geo_params = self.introspection.get_geometry_type( + table_name, geo_col + ) if geo_params: kwargs.update(geo_params) - reverse_type = 'django.contrib.gis.db.models.fields.%s' % reverse_type + reverse_type = "django.contrib.gis.db.models.fields.%s" % reverse_type - extra_kwargs = self.get_field_db_type_kwargs(kwargs, description, field, table_name, reverse_type) + extra_kwargs = self.get_field_db_type_kwargs( + kwargs, description, field, table_name, reverse_type + ) kwargs.update(extra_kwargs) field_class = self.get_field_class(reverse_type) @@ -353,8 +410,12 @@ def get_field_db_type(self, description, field=None, table_name=None): tablespace = field.db_tablespace if not tablespace: tablespace = "public" - if (tablespace, table_name, field.column) in self.unsigned and self.unsigned_suffix not in field_db_type: - field_db_type = '%s %s' % (field_db_type, self.unsigned_suffix) + if ( + tablespace, + table_name, + field.column, + ) in self.unsigned and self.unsigned_suffix not in field_db_type: + field_db_type = "%s %s" % (field_db_type, self.unsigned_suffix) return field_db_type @@ -362,8 +423,8 @@ def get_field_db_type_lookup(self, type_code): return None def get_field_class(self, class_path): - if '.' in class_path: - module_path, package_name = class_path.rsplit('.', 1) + if "." in class_path: + module_path, package_name = class_path.rsplit(".", 1) module = importlib.import_module(module_path) return getattr(module, package_name) @@ -374,13 +435,34 @@ def get_field_db_nullable(self, field, table_name): if tablespace == "": tablespace = "public" attname = field.db_column or field.attname - return self.null.get((tablespace, table_name, attname), 'fixme') + return self.null.get((tablespace, table_name, attname), "fixme") def strip_parameters(self, field_type): - if field_type and field_type != 'double precision': + if field_type and field_type != "double precision": return field_type.split(" ")[0].split("(")[0].lower() return field_type + def get_index_together(self, meta): + indexes_normalized = [] + + if hasattr(meta, "index_together"): + # Django 4.2 deprecated index_together + 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): @@ -389,30 +471,56 @@ def expand_together(self, together, meta): ) return new_together - def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, table_name, skip_list=None): + def find_unique_missing_in_db( + self, meta, table_indexes, table_constraints, table_name, skip_list=None + ): schema_editor = connection.SchemaEditorClass(connection) for field in all_local_fields(meta): if skip_list and field.attname in skip_list: continue if field.unique and meta.managed: attname = field.db_column or field.attname - db_field_unique = table_indexes.get(attname, {}).get('unique') + db_field_unique = table_indexes.get(attname, {}).get("unique") if not db_field_unique and table_constraints: - db_field_unique = any(constraint['unique'] for contraint_name, constraint in table_constraints.items() if [attname] == constraint['columns']) + db_field_unique = any( + constraint["unique"] + for contraint_name, constraint in table_constraints.items() + if [attname] == constraint["columns"] + ) if attname in table_indexes and db_field_unique: continue index_name = schema_editor._create_index_name(table_name, [attname]) - self.add_difference('unique-missing-in-db', table_name, [attname], index_name + "_uniq") + self.add_difference( + "unique-missing-in-db", table_name, [attname], index_name + "_uniq" + ) db_type = field.db_type(connection=connection) - if db_type.startswith('varchar'): - self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' varchar_pattern_ops') - 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) - db_unique_columns = normalize_together([v['columns'] for v in table_constraints.values() if v['unique'] and not v['index']]) + if db_type.startswith("varchar"): + self.add_difference( + "index-missing-in-db", + table_name, + [attname], + index_name + "_like", + " varchar_pattern_ops", + ) + if db_type.startswith("text"): + self.add_difference( + "index-missing-in-db", + table_name, + [attname], + index_name + "_like", + " text_pattern_ops", + ) + + 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: if unique_columns in db_unique_columns: @@ -423,20 +531,24 @@ def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, tabl index_name = schema_editor._create_index_name(table_name, unique_columns) - self.add_difference('unique-missing-in-db', table_name, unique_columns, index_name + "_uniq") + self.add_difference( + "unique-missing-in-db", table_name, unique_columns, index_name + "_uniq" + ) - def find_unique_missing_in_model(self, meta, table_indexes, table_constraints, table_name): + 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']: + if not constraint["unique"]: continue - if constraint['index']: + if constraint["index"]: # unique indexes are handled by find_index_missing_in_model continue - columns = constraint['columns'] + columns = constraint["columns"] if len(columns) == 1: field = fields.get(columns[0]) if field is None: @@ -447,78 +559,117 @@ def find_unique_missing_in_model(self, meta, table_indexes, table_constraints, t if tuple(columns) in unique_together: continue - self.add_difference('unique-missing-in-model', table_name, constraint_name) + self.add_difference("unique-missing-in-model", table_name, constraint_name) - def find_index_missing_in_db(self, meta, table_indexes, table_constraints, table_name): + def find_index_missing_in_db( + self, meta, table_indexes, table_constraints, table_name + ): schema_editor = connection.SchemaEditorClass(connection) for field in all_local_fields(meta): if field.db_index: attname = field.db_column or field.attname if attname not in table_indexes: index_name = schema_editor._create_index_name(table_name, [attname]) - self.add_difference('index-missing-in-db', table_name, [attname], index_name, '') + self.add_difference( + "index-missing-in-db", table_name, [attname], index_name, "" + ) db_type = field.db_type(connection=connection) - if db_type.startswith('varchar'): - self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' varchar_pattern_ops') - 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) - db_index_together = normalize_together([v['columns'] for v in table_constraints.values() if v['index'] and not v['unique']]) + if db_type.startswith("varchar"): + self.add_difference( + "index-missing-in-db", + table_name, + [attname], + index_name + "_like", + " varchar_pattern_ops", + ) + if db_type.startswith("text"): + self.add_difference( + "index-missing-in-db", + table_name, + [attname], + index_name + "_like", + " text_pattern_ops", + ) + + 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: continue index_name = schema_editor._create_index_name(table_name, columns) - self.add_difference('index-missing-in-db', table_name, columns, index_name + "_idx", '') + self.add_difference( + "index-missing-in-db", table_name, columns, index_name + "_idx", "" + ) for index in meta.indexes: if index.name not in table_constraints: - self.add_difference('index-missing-in-db', table_name, index.fields, index.name, '') + self.add_difference( + "index-missing-in-db", table_name, index.fields, index.name, "" + ) - def find_index_missing_in_model(self, meta, table_indexes, table_constraints, table_name): + 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: continue - if constraint['unique'] and not constraint['index']: + if constraint["unique"] and not constraint["index"]: # unique constraints are handled by find_unique_missing_in_model continue - columns = constraint['columns'] + columns = constraint["columns"] field = fields.get(columns[0]) - if (constraint['unique'] and constraint['index']) or field is None: + if (constraint["unique"] and constraint["index"]) or field is None: # unique indexes do not exist in django ? only unique constraints pass elif len(columns) == 1: - if constraint['primary_key'] and field.primary_key: + if constraint["primary_key"] and field.primary_key: continue - if constraint['foreign_key'] and isinstance(field, models.ForeignKey) and field.db_constraint: + if ( + constraint["foreign_key"] + and isinstance(field, models.ForeignKey) + and field.db_constraint + ): continue - if constraint['unique'] and field.unique: + if constraint["unique"] and field.unique: continue - if constraint['index'] and constraint['type'] == 'idx' and constraint.get('orders') and field.unique: - # django automatically creates a _like varchar_pattern_ops/text_pattern_ops index see https://code.djangoproject.com/ticket/12234 - # note: mysql does not have and/or introspect and fill the 'orders' attribute of constraint information + if ( + constraint["index"] + and constraint["type"] == "idx" + and constraint.get("orders") + and field.unique + ): + # django automatically creates a _like varchar_pattern_ops + # / text_pattern_ops index see https://code.djangoproject.com/ticket/12234 + # note: mysql does not have and/or introspect and fill the 'orders' + # attribute of constraint information continue - if constraint['index'] and field.db_index: + if constraint["index"] and field.db_index: continue - if constraint['check'] and field.db_check(connection=connection): + if constraint["check"] and field.db_check(connection=connection): continue - if getattr(field, 'spatial_index', False): + if getattr(field, "spatial_index", False): continue else: - if constraint['index'] and tuple(columns) in index_together: + if constraint["index"] and tuple(columns) in index_together: continue - self.add_difference('index-missing-in-model', table_name, constraint_name) + self.add_difference("index-missing-in-model", table_name, constraint_name) def find_field_missing_in_model(self, fieldmap, table_description, table_name): for row in table_description: if row[0] not in fieldmap: - self.add_difference('field-missing-in-model', table_name, row[0]) + self.add_difference("field-missing-in-model", table_name, row[0]) def find_field_missing_in_db(self, fieldmap, table_description, table_name): db_fields = [row[0] for row in table_description] @@ -527,15 +678,24 @@ def find_field_missing_in_db(self, fieldmap, table_description, table_name): field_output = [] if field.remote_field: - field_output.extend([field.remote_field.model._meta.db_table, field.remote_field.model._meta.get_field(field.remote_field.field_name).column]) - op = 'fkey-missing-in-db' + field_output.extend( + [ + field.remote_field.model._meta.db_table, + field.remote_field.model._meta.get_field( + field.remote_field.field_name + ).column, + ] + ) + op = "fkey-missing-in-db" else: - op = 'field-missing-in-db' + op = "field-missing-in-db" field_output.append(field.db_type(connection=connection)) - if self.options['include_defaults'] and field.has_default(): - field_output.append('DEFAULT %s' % field.get_prep_value(field.get_default())) + if self.options["include_defaults"] and field.has_default(): + field_output.append( + "DEFAULT %s" % field.get_prep_value(field.get_default()) + ) if not field.null: - field_output.append('NOT NULL') + field_output.append("NOT NULL") self.add_difference(op, table_name, field_name, *field_output) self.new_db_fields.add((table_name, field_name)) @@ -553,10 +713,19 @@ def find_field_type_differ(self, meta, table_description, table_name, func=None) if func: model_type, db_type = func(field, description, model_type, db_type) - if not self.strip_parameters(db_type) == self.strip_parameters(model_type): - self.add_difference('field-type-differ', table_name, field.name, model_type, db_type) + if not self.strip_parameters(db_type) == self.strip_parameters( + model_type + ) and (db_type, model_type) not in { + ("serial", "integer"), + ("bigserial", "bigint"), + }: + self.add_difference( + "field-type-differ", table_name, field.name, model_type, db_type + ) - def find_field_parameter_differ(self, meta, table_description, table_name, func=None): + def find_field_parameter_differ( + self, meta, table_description, table_name, func=None + ): db_fields = dict([(row[0], row) for row in table_description]) for field in all_local_fields(meta): if field.name not in db_fields: @@ -573,15 +742,21 @@ def find_field_parameter_differ(self, meta, table_description, table_name, func= if func: model_type, db_type = func(field, description, model_type, db_type) - model_check = field.db_parameters(connection=connection)['check'] - if ' CHECK' in db_type: + model_check = field.db_parameters(connection=connection)["check"] + if " CHECK" in db_type: db_type, db_check = db_type.split(" CHECK", 1) db_check = db_check.strip().lstrip("(").rstrip(")") else: db_check = None if not model_type == db_type or not model_check == db_check: - self.add_difference('field-parameter-differ', table_name, field.name, model_type, db_type) + self.add_difference( + "field-parameter-differ", + table_name, + field.name, + model_type, + db_type, + ) def find_field_notnull_differ(self, meta, table_description, table_name): if not self.can_detect_notnull_differ: @@ -593,18 +768,21 @@ def find_field_notnull_differ(self, meta, table_description, table_name): continue null = self.get_field_db_nullable(field, table_name) if field.null != null: - action = field.null and 'DROP' or 'SET' - self.add_difference('notnull-differ', table_name, attname, action) + action = field.null and "DROP" or "SET" + self.add_difference("notnull-differ", table_name, attname, action) def get_constraints(self, cursor, table_name, introspection): return {} def find_differences(self): - if self.options['all_applications']: + if self.options["all_applications"]: self.add_app_model_marker(None, None) for table in self.db_tables: - if table not in self.django_tables and table not in self.IGNORE_MISSING_TABLES: - self.add_difference('table-missing-in-model', table) + if ( + table not in self.django_tables + and table not in self.IGNORE_MISSING_TABLES + ): + self.add_difference("table-missing-in-model", table) cur_app_label = None for app_model in self.app_models: @@ -612,7 +790,7 @@ def find_differences(self): table_name = meta.db_table app_label = meta.app_label - if not self.options['include_proxy_models'] and meta.proxy: + if not self.options["include_proxy_models"] and meta.proxy: continue if cur_app_label != app_label: @@ -621,45 +799,61 @@ def find_differences(self): if table_name not in self.db_tables: # Table is missing from database - self.add_difference('table-missing-in-db', table_name) + self.add_difference("table-missing-in-db", table_name) continue - if hasattr(self.introspection, 'get_constraints'): - table_constraints = self.introspection.get_constraints(self.cursor, table_name) + if hasattr(self.introspection, "get_constraints"): + table_constraints = self.introspection.get_constraints( + self.cursor, table_name + ) else: - table_constraints = self.get_constraints(self.cursor, table_name, self.introspection) + table_constraints = self.get_constraints( + self.cursor, table_name, self.introspection + ) - fieldmap = dict([(field.db_column or field.get_attname(), field) for field in all_local_fields(meta)]) + fieldmap = dict( + [ + (field.db_column or field.get_attname(), field) + for field in all_local_fields(meta) + ] + ) # add ordering field if model uses order_with_respect_to if meta.order_with_respect_to: - fieldmap['_order'] = ORDERING_FIELD + fieldmap["_order"] = ORDERING_FIELD try: - table_description = self.introspection.get_table_description(self.cursor, table_name) + table_description = self.introspection.get_table_description( + self.cursor, table_name + ) except Exception as e: - self.add_difference('error', 'unable to introspect table: %s' % str(e).strip()) + self.add_difference( + "error", "unable to introspect table: %s" % str(e).strip() + ) 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(): - - columns = dct['columns'] + columns = dct["columns"] if len(columns) == 1: table_indexes[columns[0]] = { - 'primary_key': dct['primary_key'], - 'unique': dct['unique'], - 'type': dct.get('type'), - 'contraint_name': contraint_name, + "primary_key": dct["primary_key"], + "unique": dct["unique"], + "type": dct.get("type"), + "contraint_name": contraint_name, } # Fields which are defined in database but not in model # 1) find: 'unique-missing-in-model' - self.find_unique_missing_in_model(meta, table_indexes, table_constraints, table_name) + self.find_unique_missing_in_model( + meta, table_indexes, table_constraints, table_name + ) # 2) find: 'index-missing-in-model' - self.find_index_missing_in_model(meta, table_indexes, table_constraints, table_name) + self.find_index_missing_in_model( + meta, table_indexes, table_constraints, table_name + ) # 3) find: 'field-missing-in-model' self.find_field_missing_in_model(fieldmap, table_description, table_name) @@ -667,9 +861,13 @@ def find_differences(self): # 4) find: 'field-missing-in-db' self.find_field_missing_in_db(fieldmap, table_description, table_name) # 5) find: 'unique-missing-in-db' - self.find_unique_missing_in_db(meta, table_indexes, table_constraints, table_name) + self.find_unique_missing_in_db( + meta, table_indexes, table_constraints, table_name + ) # 6) find: 'index-missing-in-db' - self.find_index_missing_in_db(meta, table_indexes, table_constraints, table_name) + self.find_index_missing_in_db( + meta, table_indexes, table_constraints, table_name + ) # Fields which have a different type or parameters # 7) find: 'type-differs' @@ -678,22 +876,34 @@ def find_differences(self): self.find_field_parameter_differ(meta, table_description, table_name) # 9) find: 'field-notnull' self.find_field_notnull_differ(meta, table_description, table_name) - self.has_differences = max([len(diffs) for _app_label, _model_name, diffs in self.differences]) + self.has_differences = max( + [len(diffs) for _app_label, _model_name, diffs in self.differences] + ) def print_diff(self, style=no_style()): - """ Print differences to stdout """ - if self.options['sql']: + """Print differences to stdout""" + if self.options["sql"]: self.print_diff_sql(style) else: self.print_diff_text(style) def print_diff_text(self, style): if not self.can_detect_notnull_differ: - self.stdout.write(style.NOTICE("# Detecting notnull changes not implemented for this database backend")) + self.stdout.write( + style.NOTICE( + "# Detecting notnull changes not implemented for this " + "database backend" + ) + ) self.stdout.write("") if not self.can_detect_unsigned_differ: - self.stdout.write(style.NOTICE("# Detecting unsigned changes not implemented for this database backend")) + self.stdout.write( + style.NOTICE( + "# Detecting unsigned changes not implemented for this " + "database backend" + ) + ) self.stdout.write("") cur_app_label = None @@ -701,28 +911,59 @@ def print_diff_text(self, style): if not diffs: continue if not self.dense and app_label and cur_app_label != app_label: - self.stdout.write("%s %s" % (style.NOTICE("+ Application:"), style.SQL_TABLE(app_label))) + self.stdout.write( + "%s %s" + % (style.NOTICE("+ Application:"), style.SQL_TABLE(app_label)) + ) cur_app_label = app_label if not self.dense and model_name: - self.stdout.write("%s %s" % (style.NOTICE("|-+ Differences for model:"), style.SQL_TABLE(model_name))) + self.stdout.write( + "%s %s" + % ( + style.NOTICE("|-+ Differences for model:"), + style.SQL_TABLE(model_name), + ) + ) for diff in diffs: diff_type, diff_args = diff text = self.DIFF_TEXTS[diff_type] % dict( - (str(i), style.SQL_TABLE(', '.join(e) if isinstance(e, (list, tuple)) else e)) + ( + str(i), + style.SQL_TABLE( + ", ".join(e) if isinstance(e, (list, tuple)) else e + ), + ) for i, e in enumerate(diff_args) ) - text = "'".join(i % 2 == 0 and style.ERROR(e) or e for i, e in enumerate(text.split("'"))) + text = "'".join( + i % 2 == 0 and style.ERROR(e) or e + for i, e in enumerate(text.split("'")) + ) if not self.dense: self.stdout.write("%s %s" % (style.NOTICE("|--+"), text)) else: if app_label: - self.stdout.write("%s %s %s %s %s" % (style.NOTICE("App"), style.SQL_TABLE(app_label), style.NOTICE('Model'), style.SQL_TABLE(model_name), text)) + self.stdout.write( + "%s %s %s %s %s" + % ( + style.NOTICE("App"), + style.SQL_TABLE(app_label), + style.NOTICE("Model"), + style.SQL_TABLE(model_name), + text, + ) + ) else: self.stdout.write(text) def print_diff_sql(self, style): if not self.can_detect_notnull_differ: - self.stdout.write(style.NOTICE("-- Detecting notnull changes not implemented for this database backend")) + self.stdout.write( + style.NOTICE( + "-- Detecting notnull changes not implemented for this " + "database backend" + ) + ) self.stdout.write("") cur_app_label = None @@ -736,10 +977,14 @@ def print_diff_sql(self, style): if not diffs: continue if not self.dense and cur_app_label != app_label: - self.stdout.write(style.NOTICE("-- Application: %s" % style.SQL_TABLE(app_label))) + self.stdout.write( + style.NOTICE("-- Application: %s" % style.SQL_TABLE(app_label)) + ) cur_app_label = app_label if not self.dense and model_name: - self.stdout.write(style.NOTICE("-- Model: %s" % style.SQL_TABLE(model_name))) + self.stdout.write( + style.NOTICE("-- Model: %s" % style.SQL_TABLE(model_name)) + ) for diff in diffs: diff_type, diff_args = diff text = self.DIFF_SQL[diff_type](style, qn, diff_args) @@ -763,7 +1008,7 @@ def load_unsigned(self): class MySQLDiff(SQLDiff): can_detect_notnull_differ = True can_detect_unsigned_differ = True - unsigned_suffix = 'UNSIGNED' + unsigned_suffix = "UNSIGNED" def load(self): super().load() @@ -774,40 +1019,49 @@ def format_field_names(self, field_names): return [f.lower() for f in field_names] def load_null(self): - tablespace = 'public' + tablespace = "public" for table_name in self.db_tables: - result = self.sql_to_dict(""" + result = self.sql_to_dict( + """ SELECT column_name, is_nullable FROM information_schema.columns WHERE table_schema = DATABASE() - AND table_name = %s""", [table_name]) + AND table_name = %s""", + [table_name], + ) for table_info in result: - key = (tablespace, table_name, table_info['column_name']) - self.null[key] = table_info['is_nullable'] == 'YES' + key = (tablespace, table_name, table_info["column_name"]) + self.null[key] = table_info["is_nullable"] == "YES" def load_unsigned(self): - tablespace = 'public' + tablespace = "public" for table_name in self.db_tables: - result = self.sql_to_dict(""" + result = self.sql_to_dict( + """ SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = %s - AND column_type LIKE '%%unsigned'""", [table_name]) + AND column_type LIKE '%%unsigned'""", + [table_name], + ) for table_info in result: - key = (tablespace, table_name, table_info['column_name']) + key = (tablespace, table_name, table_info["column_name"]) self.unsigned.add(key) def load_auto_increment(self): for table_name in self.db_tables: - result = self.sql_to_dict(""" + result = self.sql_to_dict( + """ SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = %s - AND extra = 'auto_increment'""", [table_name]) + AND extra = 'auto_increment'""", + [table_name], + ) for table_info in result: - key = (table_name, table_info['column_name']) + key = (table_name, table_info["column_name"]) self.auto_increment.add(key) # All the MySQL hacks together create something of a problem @@ -822,92 +1076,138 @@ def get_field_db_type(self, description, field=None, table_name=None): field_type = self.get_field_model_type(field) # Fix char/varchar inconsistencies - if self.strip_parameters(field_type) == 'char' and self.strip_parameters(db_type) == 'varchar': + if ( + self.strip_parameters(field_type) == "char" + and self.strip_parameters(db_type) == "varchar" + ): db_type = db_type.lstrip("var") - # They like to call bools various integer types and introspection makes that a integer - # just convert them all to bools - if self.strip_parameters(field_type) == 'bool': - if db_type == 'integer': - db_type = 'bool' - - if (table_name, field.column) in self.auto_increment and 'AUTO_INCREMENT' not in db_type: - db_type += ' AUTO_INCREMENT' + # They like to call bools various integer types and introspection makes + # that a integer just convert them all to bools + if self.strip_parameters(field_type) == "bool": + if db_type == "integer": + db_type = "bool" + + if ( + table_name, + field.column, + ) in self.auto_increment and "AUTO_INCREMENT" not in db_type: + db_type += " AUTO_INCREMENT" return db_type - def find_index_missing_in_model(self, meta, table_indexes, table_constraints, table_name): + 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: continue - if constraint['unique'] and not constraint['index']: + if constraint["unique"] and not constraint["index"]: # unique constraints are handled by find_unique_missing_in_model continue - columns = constraint['columns'] + columns = constraint["columns"] field = fields.get(columns[0]) # extra check removed from superclass here, otherwise function is the same if len(columns) == 1: if not field: # both index and field are missing from the model - self.add_difference('index-missing-in-model', table_name, constraint_name) + self.add_difference( + "index-missing-in-model", table_name, constraint_name + ) continue - if constraint['primary_key'] and field.primary_key: + if constraint["primary_key"] and field.primary_key: continue - if constraint['foreign_key'] and isinstance(field, models.ForeignKey) and field.db_constraint: + if ( + constraint["foreign_key"] + and isinstance(field, models.ForeignKey) + and field.db_constraint + ): continue - if constraint['unique'] and field.unique: + if constraint["unique"] and field.unique: continue - if constraint['index'] and constraint['type'] == 'idx' and constraint.get('orders') and field.unique: - # django automatically creates a _like varchar_pattern_ops/text_pattern_ops index see https://code.djangoproject.com/ticket/12234 - # note: mysql does not have and/or introspect and fill the 'orders' attribute of constraint information + if ( + constraint["index"] + and constraint["type"] == "idx" + and constraint.get("orders") + and field.unique + ): + # django automatically creates a _like varchar_pattern_ops + # / text_pattern_ops index see https://code.djangoproject.com/ticket/12234 + # note: mysql does not have and/or introspect and fill the 'orders' + # attribute of constraint information continue - if constraint['index'] and field.db_index: + if constraint["index"] and field.db_index: continue - if constraint['check'] and field.db_check(connection=connection): + if constraint["check"] and field.db_check(connection=connection): continue - if getattr(field, 'spatial_index', False): + if getattr(field, "spatial_index", False): continue else: - if constraint['index'] and tuple(columns) in index_together: + if constraint["index"] and tuple(columns) in index_together: continue - if constraint['index'] and constraint['unique'] and tuple(columns) in unique_together: + if ( + constraint["index"] + and constraint["unique"] + and tuple(columns) in unique_together + ): continue - self.add_difference('index-missing-in-model', table_name, constraint_name) - - def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, table_name, skip_list=None): + self.add_difference("index-missing-in-model", table_name, constraint_name) + def find_unique_missing_in_db( + self, meta, table_indexes, table_constraints, table_name, skip_list=None + ): schema_editor = connection.SchemaEditorClass(connection) for field in all_local_fields(meta): if skip_list and field.attname in skip_list: continue if field.unique and meta.managed: attname = field.db_column or field.attname - db_field_unique = table_indexes.get(attname, {}).get('unique') + db_field_unique = table_indexes.get(attname, {}).get("unique") if not db_field_unique and table_constraints: - db_field_unique = any(constraint['unique'] for contraint_name, constraint in table_constraints.items() if [attname] == constraint['columns']) + db_field_unique = any( + constraint["unique"] + for contraint_name, constraint in table_constraints.items() + if [attname] == constraint["columns"] + ) if attname in table_indexes and db_field_unique: continue index_name = schema_editor._create_index_name(table_name, [attname]) - self.add_difference('unique-missing-in-db', table_name, [attname], index_name + "_uniq") + self.add_difference( + "unique-missing-in-db", table_name, [attname], index_name + "_uniq" + ) db_type = field.db_type(connection=connection) - if db_type.startswith('varchar'): - self.add_difference('index-missing-in-db', table_name, [attname], index_name + '_like', ' varchar_pattern_ops') - 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) + if db_type.startswith("varchar"): + self.add_difference( + "index-missing-in-db", + table_name, + [attname], + index_name + "_like", + " varchar_pattern_ops", + ) + if db_type.startswith("text"): + self.add_difference( + "index-missing-in-db", + table_name, + [attname], + index_name + "_like", + " text_pattern_ops", + ) + + 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']]) + db_unique_columns = normalize_together( + [v["columns"] for v in table_constraints.values() if v["unique"]] + ) for unique_columns in unique_together: if unique_columns in db_unique_columns: @@ -917,7 +1217,9 @@ def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, tabl continue index_name = schema_editor._create_index_name(table_name, unique_columns) - self.add_difference('unique-missing-in-db', table_name, unique_columns, index_name + "_uniq") + self.add_difference( + "unique-missing-in-db", table_name, unique_columns, index_name + "_uniq" + ) class SqliteSQLDiff(SQLDiff): @@ -929,10 +1231,12 @@ def load_null(self): # sqlite does not support tablespaces tablespace = "public" # index, column_name, column_type, nullable, default_value - # see: http://www.sqlite.org/pragma.html#pragma_table_info - for table_info in self.sql_to_dict("PRAGMA table_info('%s');" % table_name, []): - key = (tablespace, table_name, table_info['name']) - self.null[key] = not table_info['notnull'] + # see: https://www.sqlite.org/pragma.html#pragma_table_info + for table_info in self.sql_to_dict( + "PRAGMA table_info('%s');" % table_name, [] + ): + key = (tablespace, table_name, table_info["name"]) + self.null[key] = not table_info["notnull"] def load_unsigned(self): pass @@ -940,34 +1244,50 @@ def load_unsigned(self): # Unique does not seem to be implied on Sqlite for Primary_key's # if this is more generic among databases this might be usefull # to add to the superclass's find_unique_missing_in_db method - def find_unique_missing_in_db(self, meta, table_indexes, table_constraints, table_name, skip_list=None): + def find_unique_missing_in_db( + self, meta, table_indexes, table_constraints, table_name, skip_list=None + ): if skip_list is None: skip_list = [] - unique_columns = [field.db_column or field.attname for field in all_local_fields(meta) if field.unique] + unique_columns = [ + field.db_column or field.attname + for field in all_local_fields(meta) + if field.unique + ] for constraint in table_constraints.values(): - columns = constraint['columns'] + columns = constraint["columns"] if len(columns) == 1: column = columns[0] - if column in unique_columns and (constraint['unique'] or constraint['primary_key']): + 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) - db_unique_columns = normalize_together([v['columns'] for v in table_constraints.values() if v['unique']]) + 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: if unique_columns in db_unique_columns: skip_list.append(unique_columns) - super().find_unique_missing_in_db(meta, table_indexes, table_constraints, table_name, skip_list=skip_list) + super().find_unique_missing_in_db( + meta, table_indexes, table_constraints, table_name, skip_list=skip_list + ) # Finding Indexes by using the get_indexes dictionary doesn't seem to work # for sqlite. - def find_index_missing_in_db(self, meta, table_indexes, table_constraints, table_name): + def find_index_missing_in_db( + self, meta, table_indexes, table_constraints, table_name + ): pass - def find_index_missing_in_model(self, meta, table_indexes, table_constraints, table_name): + def find_index_missing_in_model( + self, meta, table_indexes, table_constraints, table_name + ): pass def get_field_db_type(self, description, field=None, table_name=None): @@ -977,7 +1297,10 @@ def get_field_db_type(self, description, field=None, table_name=None): if field: field_type = self.get_field_model_type(field) # Fix char/varchar inconsistencies - if self.strip_parameters(field_type) == 'char' and self.strip_parameters(db_type) == 'varchar': + if ( + self.strip_parameters(field_type) == "char" + and self.strip_parameters(db_type) == "varchar" + ): db_type = db_type.lstrip("var") return db_type @@ -987,8 +1310,8 @@ class PostgresqlSQLDiff(SQLDiff): can_detect_unsigned_differ = True DATA_TYPES_REVERSE_NAME = { - 'hstore': 'django.contrib.postgres.fields.HStoreField', - 'jsonb': 'django.contrib.postgres.fields.JSONField', + "hstore": "django.contrib.postgres.fields.HStoreField", + "jsonb": "django.contrib.postgres.fields.JSONField", } # Hopefully in the future we can add constraint checking and other more @@ -1000,7 +1323,7 @@ class PostgresqlSQLDiff(SQLDiff): INNER JOIN pg_class ON conrelid=pg_class.oid INNER JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace ORDER BY CASE WHEN contype='f' THEN 0 ELSE 1 END,contype,nspname,relname,conname; - """ + """ # noqa: E501 SQL_LOAD_NULL = """ SELECT nspname, relname, attname, attnotnull FROM pg_attribute @@ -1008,9 +1331,33 @@ class PostgresqlSQLDiff(SQLDiff): INNER JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace; """ - SQL_FIELD_TYPE_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % (style.SQL_KEYWORD('ALTER TABLE'), style.SQL_TABLE(qn(args[0])), style.SQL_KEYWORD('ALTER'), style.SQL_FIELD(qn(args[1])), style.SQL_KEYWORD("TYPE"), style.SQL_COLTYPE(args[2])) - SQL_FIELD_PARAMETER_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % (style.SQL_KEYWORD('ALTER TABLE'), style.SQL_TABLE(qn(args[0])), style.SQL_KEYWORD('ALTER'), style.SQL_FIELD(qn(args[1])), style.SQL_KEYWORD("TYPE"), style.SQL_COLTYPE(args[2])) - SQL_NOTNULL_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % (style.SQL_KEYWORD('ALTER TABLE'), style.SQL_TABLE(qn(args[0])), style.SQL_KEYWORD('ALTER COLUMN'), style.SQL_FIELD(qn(args[1])), style.SQL_KEYWORD(args[2]), style.SQL_KEYWORD('NOT NULL')) + SQL_FIELD_TYPE_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % ( + style.SQL_KEYWORD("ALTER TABLE"), + style.SQL_TABLE(qn(args[0])), + style.SQL_KEYWORD("ALTER"), + style.SQL_FIELD(qn(args[1])), + style.SQL_KEYWORD("TYPE"), + style.SQL_COLTYPE(args[2]), + ) + SQL_FIELD_PARAMETER_DIFFER = ( + lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" + % ( + style.SQL_KEYWORD("ALTER TABLE"), + style.SQL_TABLE(qn(args[0])), + style.SQL_KEYWORD("ALTER"), + style.SQL_FIELD(qn(args[1])), + style.SQL_KEYWORD("TYPE"), + style.SQL_COLTYPE(args[2]), + ) + ) + SQL_NOTNULL_DIFFER = lambda self, style, qn, args: "%s %s\n\t%s %s %s %s;" % ( + style.SQL_KEYWORD("ALTER TABLE"), + style.SQL_TABLE(qn(args[0])), + style.SQL_KEYWORD("ALTER COLUMN"), + style.SQL_FIELD(qn(args[1])), + style.SQL_KEYWORD(args[2]), + style.SQL_KEYWORD("NOT NULL"), + ) def load(self): super().load() @@ -1019,8 +1366,8 @@ def load(self): def load_null(self): for dct in self.sql_to_dict(self.SQL_LOAD_NULL, []): - key = (dct['nspname'], dct['relname'], dct['attname']) - self.null[key] = not dct['attnotnull'] + key = (dct["nspname"], dct["relname"], dct["attname"]) + self.null[key] = not dct["attnotnull"] def load_unsigned(self): # PostgreSQL does not support unsigned, so no columns are @@ -1029,42 +1376,42 @@ def load_unsigned(self): def load_constraints(self): for dct in self.sql_to_dict(self.SQL_LOAD_CONSTRAINTS, []): - key = (dct['nspname'], dct['relname'], dct['attname']) - if 'CHECK' in dct['pg_get_constraintdef']: + key = (dct["nspname"], dct["relname"], dct["attname"]) + if "CHECK" in dct["pg_get_constraintdef"]: self.check_constraints[key] = dct def get_data_type_arrayfield(self, base_field): return { - 'name': 'django.contrib.postgres.fields.ArrayField', - 'kwargs': { - 'base_field': self.get_field_class(base_field)(), + "name": "django.contrib.postgres.fields.ArrayField", + "kwargs": { + "base_field": self.get_field_class(base_field)(), }, } def get_data_types_reverse_override(self): return { - 1042: 'CharField', - 1000: lambda: self.get_data_type_arrayfield(base_field='BooleanField'), - 1001: lambda: self.get_data_type_arrayfield(base_field='BinaryField'), - 1002: lambda: self.get_data_type_arrayfield(base_field='CharField'), - 1005: lambda: self.get_data_type_arrayfield(base_field='IntegerField'), - 1006: lambda: self.get_data_type_arrayfield(base_field='IntegerField'), - 1007: lambda: self.get_data_type_arrayfield(base_field='IntegerField'), - 1009: lambda: self.get_data_type_arrayfield(base_field='CharField'), - 1014: lambda: self.get_data_type_arrayfield(base_field='CharField'), - 1015: lambda: self.get_data_type_arrayfield(base_field='CharField'), - 1016: lambda: self.get_data_type_arrayfield(base_field='BigIntegerField'), - 1017: lambda: self.get_data_type_arrayfield(base_field='FloatField'), - 1021: lambda: self.get_data_type_arrayfield(base_field='FloatField'), - 1022: lambda: self.get_data_type_arrayfield(base_field='FloatField'), - 1115: lambda: self.get_data_type_arrayfield(base_field='DateTimeField'), - 1185: lambda: self.get_data_type_arrayfield(base_field='DateTimeField'), - 1231: lambda: self.get_data_type_arrayfield(base_field='DecimalField'), - # {'name': 'django.contrib.postgres.fields.ArrayField', 'kwargs': {'base_field': 'IntegerField'}}, - 1186: lambda: self.get_data_type_arrayfield(base_field='DurationField'), + 1042: "CharField", + 1000: lambda: self.get_data_type_arrayfield(base_field="BooleanField"), + 1001: lambda: self.get_data_type_arrayfield(base_field="BinaryField"), + 1002: lambda: self.get_data_type_arrayfield(base_field="CharField"), + 1005: lambda: self.get_data_type_arrayfield(base_field="IntegerField"), + 1006: lambda: self.get_data_type_arrayfield(base_field="IntegerField"), + 1007: lambda: self.get_data_type_arrayfield(base_field="IntegerField"), + 1009: lambda: self.get_data_type_arrayfield(base_field="CharField"), + 1014: lambda: self.get_data_type_arrayfield(base_field="CharField"), + 1015: lambda: self.get_data_type_arrayfield(base_field="CharField"), + 1016: lambda: self.get_data_type_arrayfield(base_field="BigIntegerField"), + 1017: lambda: self.get_data_type_arrayfield(base_field="FloatField"), + 1021: lambda: self.get_data_type_arrayfield(base_field="FloatField"), + 1022: lambda: self.get_data_type_arrayfield(base_field="FloatField"), + 1115: lambda: self.get_data_type_arrayfield(base_field="DateTimeField"), + 1185: lambda: self.get_data_type_arrayfield(base_field="DateTimeField"), + 1231: lambda: self.get_data_type_arrayfield(base_field="DecimalField"), + # {'name': 'django.contrib.postgres.fields.ArrayField', 'kwargs': {'base_field': 'IntegerField'}}, # noqa: E501 + 1186: lambda: self.get_data_type_arrayfield(base_field="DurationField"), # 1186: 'django.db.models.fields.DurationField', - 3614: 'django.contrib.postgres.search.SearchVectorField', - 3802: 'django.contrib.postgres.fields.JSONField', + 3614: "django.contrib.postgres.search.SearchVectorField", + 3802: "django.contrib.postgres.fields.JSONField", } def get_constraints(self, cursor, table_name, introspection): @@ -1076,7 +1423,8 @@ def get_constraints(self, cursor, table_name, introspection): constraints = {} # Loop over the key table, collecting things as constraints # This will get PKs, FKs, and uniques, but not CHECK - cursor.execute(""" + cursor.execute( + """ SELECT kc.constraint_name, kc.column_name, @@ -1090,7 +1438,9 @@ def get_constraints(self, cursor, table_name, introspection): WHERE kc.table_schema = %s AND kc.table_name = %s - """, ["public", table_name]) + """, # noqa: E501 + ["public", table_name], + ) for constraint, column, kind, used_cols in cursor.fetchall(): # If we're the first column, make the record if constraint not in constraints: @@ -1098,14 +1448,17 @@ def get_constraints(self, cursor, table_name, introspection): "columns": [], "primary_key": kind.lower() == "primary key", "unique": kind.lower() in ["primary key", "unique"], - "foreign_key": tuple(used_cols[0].split(".", 1)) if kind.lower() == "foreign key" else None, + "foreign_key": tuple(used_cols[0].split(".", 1)) + if kind.lower() == "foreign key" + else None, "check": False, "index": False, } # Record the details - constraints[constraint]['columns'].append(column) + constraints[constraint]["columns"].append(column) # Now get CHECK constraint columns - cursor.execute(""" + cursor.execute( + """ SELECT kc.constraint_name, kc.column_name FROM information_schema.constraint_column_usage AS kc JOIN information_schema.table_constraints AS c ON @@ -1116,7 +1469,9 @@ def get_constraints(self, cursor, table_name, introspection): c.constraint_type = 'CHECK' AND kc.table_schema = %s AND kc.table_name = %s - """, ["public", table_name]) + """, + ["public", table_name], + ) for constraint, column in cursor.fetchall(): # If we're the first column, make the record if constraint not in constraints: @@ -1129,9 +1484,10 @@ def get_constraints(self, cursor, table_name, introspection): "index": False, } # Record the details - constraints[constraint]['columns'].append(column) + constraints[constraint]["columns"].append(column) # Now get indexes - cursor.execute(""" + cursor.execute( + """ SELECT c2.relname, ARRAY( @@ -1145,7 +1501,9 @@ def get_constraints(self, cursor, table_name, introspection): WHERE c.oid = idx.indrelid AND idx.indexrelid = c2.oid AND c.relname = %s - """, [table_name]) + """, # noqa: E501 + [table_name], + ) for index, columns, unique, primary in cursor.fetchall(): if index not in constraints: constraints[index] = { @@ -1158,7 +1516,10 @@ def get_constraints(self, cursor, table_name, introspection): } return constraints - # def get_field_db_type_kwargs(self, current_kwargs, description, field=None, table_name=None, reverse_type=None): + # def get_field_db_type_kwargs( + # self, current_kwargs, description, field=None, + # table_name=None, reverse_type=None, + # ): # kwargs = {} # if field and 'base_field' in current_kwargs: # # find @@ -1174,10 +1535,12 @@ def get_constraints(self, cursor, table_name, introspection): # """, # (table_name, attname) # )[0]['type'] - # # TODO: this gives the concrete type that the database uses, why not use this - # # much earlier in the process to compare to whatever django spits out as - # # the database type ? - # max_length = re.search("character varying\((\d+)\)\[\]", introspect_db_type) + # # TODO: this gives the concrete type that the database uses, why not use + # # this much earlier in the process to compare to whatever django + # # spits out as the database type ? + # max_length = re.search( + # "character varying\((\d+)\)\[\]", introspect_db_type + # ) # if max_length: # kwargs['max_length'] = max_length[1] # return kwargs @@ -1188,12 +1551,14 @@ def get_field_db_type(self, description, field=None, table_name=None): return if field: if db_type.endswith("[]"): - # TODO: This is a hack for array types. Ideally we either pass the correct - # 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 - # process to compare to whatever django spits out as the desired database type ? + # TODO: This is a hack for array types. Ideally we either pass the + # correct 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 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( """SELECT attname, format_type(atttypid, atttypmod) AS type @@ -1204,36 +1569,51 @@ def get_field_db_type(self, description, field=None, table_name=None): AND NOT attisdropped ORDER BY attnum; """, - (table_name, attname) - )[0]['type'] + (table_name, attname), + )[0]["type"] if introspect_db_type.startswith("character varying"): - introspect_db_type = introspect_db_type.replace("character varying", "varchar") + introspect_db_type = introspect_db_type.replace( + "character varying", "varchar" + ) return introspect_db_type if field.primary_key and isinstance(field, AutoField): - if db_type == 'integer': - db_type = 'serial' - elif db_type == 'bigint': - db_type = 'bigserial' + # TODO: Django>4.1 uses int/bigint with identity columns + # instead of serial/bigserial + if db_type == "integer": + db_type = "serial" + elif db_type == "bigint": + db_type = "bigserial" if table_name: tablespace = field.db_tablespace if tablespace == "": tablespace = "public" attname = field.db_column or field.attname - check_constraint = self.check_constraints.get((tablespace, table_name, attname), {}).get('pg_get_constraintdef', None) + check_constraint = self.check_constraints.get( + (tablespace, table_name, attname), {} + ).get("pg_get_constraintdef", None) if check_constraint: check_constraint = check_constraint.replace("((", "(") check_constraint = check_constraint.replace("))", ")") - check_constraint = '("'.join([')' in e and '" '.join(p.strip('"') for p in e.split(" ", 1)) or e for e in check_constraint.split("(")]) + check_constraint = '("'.join( + [ + ")" in e + and '" '.join(p.strip('"') for p in e.split(" ", 1)) + or e + for e in check_constraint.split("(") + ] + ) # TODO: might be more then one constraint in definition ? - db_type += ' ' + check_constraint + db_type += " " + check_constraint return db_type def get_field_db_type_lookup(self, type_code): try: - name = self.sql_to_dict("SELECT typname FROM pg_type WHERE typelem=%s;", [type_code])[0]['typname'] - return self.DATA_TYPES_REVERSE_NAME.get(name.strip('_')) + name = self.sql_to_dict( + "SELECT typname FROM pg_type WHERE typelem=%s;", [type_code] + )[0]["typname"] + return self.DATA_TYPES_REVERSE_NAME.get(name.strip("_")) except (IndexError, KeyError): pass @@ -1248,12 +1628,12 @@ def callback(field, description, model_type, db_type): DATABASE_SQLDIFF_CLASSES = { - 'postgis': PostgresqlSQLDiff, - 'postgresql_psycopg2': PostgresqlSQLDiff, - 'postgresql': PostgresqlSQLDiff, - 'mysql': MySQLDiff, - 'sqlite3': SqliteSQLDiff, - 'oracle': GenericSQLDiff + "postgis": PostgresqlSQLDiff, + "postgresql_psycopg2": PostgresqlSQLDiff, + "postgresql": PostgresqlSQLDiff, + "mysql": MySQLDiff, + "sqlite3": SqliteSQLDiff, + "oracle": GenericSQLDiff, } @@ -1263,49 +1643,71 @@ class Command(BaseCommand): It indicates how columns in the database are different from the sql that would be generated by Django. This command is not a database migration tool. (Though it can certainly help) It's purpose is to show the current differences as a way -to check/debug ur models compared to the real database tables and columns.""" +to check/debug ur models compared to the real database tables and columns.""" # noqa: E501 output_transaction = False def add_arguments(self, parser): super().add_arguments(parser) - parser.add_argument('app_label', nargs='*') + parser.add_argument("app_label", nargs="*") parser.add_argument( - '--all-applications', '-a', action='store_true', + "--all-applications", + "-a", + action="store_true", default=False, - dest='all_applications', - help="Automaticly include all application from INSTALLED_APPS." + dest="all_applications", + help="Automaticly include all application from INSTALLED_APPS.", ) parser.add_argument( - '--not-only-existing', '-e', action='store_false', + "--not-only-existing", + "-e", + action="store_false", default=True, - dest='only_existing', - help="Check all tables that exist in the database, not only tables that should exist based on models." + dest="only_existing", + help=( + "Check all tables that exist in the database, not only tables " + "that should exist based on models." + ), ) parser.add_argument( - '--dense-output', '-d', action='store_true', dest='dense_output', + "--dense-output", + "-d", + action="store_true", + dest="dense_output", default=False, - help="Shows the output in dense format, normally output is spreaded over multiple lines." + help=( + "Shows the output in dense format, " + "normally output is spreaded over multiple lines." + ), ) parser.add_argument( - '--output_text', '-t', action='store_false', dest='sql', + "--output_text", + "-t", + action="store_false", + dest="sql", default=True, - help="Outputs the differences as descriptive text instead of SQL" + help="Outputs the differences as descriptive text instead of SQL", ) parser.add_argument( - '--include-proxy-models', action='store_true', dest='include_proxy_models', + "--include-proxy-models", + action="store_true", + dest="include_proxy_models", default=False, - help="Include proxy models in the graph" + help="Include proxy models in the graph", ) parser.add_argument( - '--include-defaults', action='store_true', dest='include_defaults', + "--include-defaults", + action="store_true", + dest="include_defaults", default=False, - help="Include default values in SQL output (beta feature)" + help="Include default values in SQL output (beta feature)", ) parser.add_argument( - '--migrate-for-tests', action='store_true', dest='migrate_for_tests', + "--migrate-for-tests", + action="store_true", + dest="migrate_for_tests", default=False, - help=argparse.SUPPRESS + help=argparse.SUPPRESS, ) def __init__(self, *args, **kwargs): @@ -1316,25 +1718,28 @@ def __init__(self, *args, **kwargs): def handle(self, *args, **options): from django.conf import settings - app_labels = options['app_label'] + app_labels = options["app_label"] engine = None - if hasattr(settings, 'DATABASES'): - engine = settings.DATABASES['default']['ENGINE'] + if hasattr(settings, "DATABASES"): + engine = settings.DATABASES["default"]["ENGINE"] else: engine = settings.DATABASE_ENGINE - if engine == 'dummy': + if engine == "dummy": # This must be the "dummy" database backend, which means the user # hasn't set DATABASE_ENGINE. - raise CommandError("""Django doesn't know which syntax to use for your SQL statements, -because you haven't specified the DATABASE_ENGINE setting. -Edit your settings file and change DATABASE_ENGINE to something like 'postgresql' or 'mysql'.""") + raise CommandError( + "Django doesn't know which syntax to use for your SQL statements, " + "because you haven't specified the DATABASE_ENGINE setting. " + "Edit your settings file and change DATABASE_ENGINE to something like " + "'postgresql' or 'mysql'." + ) - if options['all_applications']: + if options["all_applications"]: app_models = apps.get_models(include_auto_created=True) else: if not app_labels: - raise CommandError('Enter at least one appname.') + raise CommandError("Enter at least one appname.") if not isinstance(app_labels, (list, tuple, set)): app_labels = [app_labels] @@ -1345,21 +1750,24 @@ def handle(self, *args, **options): app_models.extend(app_config.get_models(include_auto_created=True)) if not app_models: - raise CommandError('Unable to execute sqldiff no models founds.') + raise CommandError("Unable to execute sqldiff no models founds.") - migrate_for_tests = options['migrate_for_tests'] + migrate_for_tests = options["migrate_for_tests"] if migrate_for_tests: from django.core.management import call_command + call_command("migrate", *app_labels, no_input=True, run_syncdb=True) if not engine: - engine = connection.__module__.split('.')[-2] + engine = connection.__module__.split(".")[-2] - if '.' in engine: - engine = engine.split('.')[-1] + if "." in engine: + engine = engine.split(".")[-1] cls = DATABASE_SQLDIFF_CLASSES.get(engine, GenericSQLDiff) - sqldiff_instance = cls(app_models, options, stdout=self.stdout, stderr=self.stderr) + sqldiff_instance = cls( + app_models, options, stdout=self.stdout, stderr=self.stderr + ) sqldiff_instance.load() sqldiff_instance.find_differences() if not sqldiff_instance.has_differences: @@ -1370,14 +1778,14 @@ def execute(self, *args, **options): try: super().execute(*args, **options) except CommandError as e: - if options['traceback']: + if options["traceback"]: raise # self.stderr is not guaranteed to be set here - stderr = getattr(self, 'stderr', None) + stderr = getattr(self, "stderr", None) if not stderr: stderr = OutputWrapper(sys.stderr, self.style.ERROR) - stderr.write('%s: %s' % (e.__class__.__name__, e)) + stderr.write("%s: %s" % (e.__class__.__name__, e)) sys.exit(2) def run_from_argv(self, argv): diff --git a/django_extensions/management/commands/sqldsn.py b/django_extensions/management/commands/sqldsn.py index dab377231..592fded98 100644 --- a/django_extensions/management/commands/sqldsn.py +++ b/django_extensions/management/commands/sqldsn.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ sqldns.py @@ -7,6 +6,7 @@ import sys import warnings +from typing import List from django.conf import settings from django.core.management.base import BaseCommand, CommandError @@ -16,49 +16,147 @@ from django_extensions.utils.deprecation import RemovedInNextVersionWarning +def _sqlite_name(dbhost, dbport, dbname, dbuser, dbpass): + return dbname + + +def _mysql_keyvalue(dbhost, dbport, dbname, dbuser, dbpass): + dsnstr = f'host="{dbhost}", db="{dbname}", user="{dbuser}", passwd="{dbpass}"' + if dbport is not None: + dsnstr += f', port="{dbport}"' + return dsnstr + + +def _mysql_args(dbhost, dbport, dbname, dbuser, dbpass): + dsnstr = f'-h "{dbhost}" -D "{dbname}" -u "{dbuser}" -p "{dbpass}"' + if dbport is not None: + dsnstr += f" -P {dbport}" + return dsnstr + + +def _postgresql_keyvalue(dbhost, dbport, dbname, dbuser, dbpass): + dsnstr = f"host='{dbhost}' dbname='{dbname}' user='{dbuser}' password='{dbpass}'" + if dbport is not None: + dsnstr += f" port='{dbport}'" + return dsnstr + + +def _postgresql_kwargs(dbhost, dbport, dbname, dbuser, dbpass): + dsnstr = ( + f"host={dbhost!r}, database={dbname!r}, user={dbuser!r}, password={dbpass!r}" + ) + if dbport is not None: + dsnstr += f", port={dbport!r}" + return dsnstr + + +def _postgresql_pgpass(dbhost, dbport, dbname, dbuser, dbpass): + return ":".join(str(s) for s in [dbhost, dbport, dbname, dbuser, dbpass]) + + +def _uri(engine): + def inner(dbhost, dbport, dbname, dbuser, dbpass): + host = dbhost or "" + if dbport is not None and dbport != "": + host += f":{dbport}" + if dbuser is not None and dbuser != "": + user = dbuser + if dbpass is not None and dbpass != "": + user += f":{dbpass}" + host = f"{user}@{host}" + return f"{engine}://{host}/{dbname}" + + return inner + + +_FORMATTERS = [ + (SQLITE_ENGINES, None, _sqlite_name), + (SQLITE_ENGINES, "filename", _sqlite_name), + (SQLITE_ENGINES, "uri", _uri("sqlite")), + (MYSQL_ENGINES, None, _mysql_keyvalue), + (MYSQL_ENGINES, "keyvalue", _mysql_keyvalue), + (MYSQL_ENGINES, "args", _mysql_args), + (MYSQL_ENGINES, "uri", _uri("mysql")), + (POSTGRESQL_ENGINES, None, _postgresql_keyvalue), + (POSTGRESQL_ENGINES, "keyvalue", _postgresql_keyvalue), + (POSTGRESQL_ENGINES, "kwargs", _postgresql_kwargs), + (POSTGRESQL_ENGINES, "uri", _uri("postgresql")), + (POSTGRESQL_ENGINES, "pgpass", _postgresql_pgpass), +] + + 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): super().add_arguments(parser) - parser.add_argument( - '-R', '--router', action='store', - dest='router', default=DEFAULT_DB_ALIAS, - help='Use this router-database other then default (deprecated: use --database instead)' + dbspec = parser.add_mutually_exclusive_group() + dbspec.add_argument( + "-R", + "--router", + action="store", + dest="router", + default=DEFAULT_DB_ALIAS, + help=( + "Use this router-database other then default " + "(deprecated: use --database instead)" + ), ) - parser.add_argument( - '--database', default=DEFAULT_DB_ALIAS, - help='Nominates a database to run command for. Defaults to the "%s" database.' % DEFAULT_DB_ALIAS, + dbspec.add_argument( + "--database", + default=DEFAULT_DB_ALIAS, + help=( + "Nominates a database to run command for. " + 'Defaults to the "%s" database.' + ) + % DEFAULT_DB_ALIAS, ) - parser.add_argument( - '-s', '--style', action='store', - dest='style', default=None, - help='DSN format style: keyvalue, uri, pgpass, all' + styles = sorted( + set([style for _, style, _ in _FORMATTERS if style is not None]) ) parser.add_argument( - '-a', '--all', action='store_true', - dest='all', default=False, - help='Show DSN for all database routes' + "-s", + "--style", + action="store", + dest="style", + default=None, + choices=styles + ["all"], + help="DSN format style.", + ) + dbspec.add_argument( + "-a", + "--all", + action="store_true", + dest="all", + default=False, + help="Show DSN for all database routes", ) parser.add_argument( - '-q', '--quiet', action='store_true', - dest='quiet', default=False, - help='Quiet mode only show DSN' + "-q", + "--quiet", + action="store_true", + dest="quiet", + default=False, + help="Quiet mode only show DSN", ) def handle(self, *args, **options): self.style = color_style() - all_databases = options['all'] + all_databases = options["all"] if all_databases: databases = settings.DATABASES.keys() else: - databases = [options['database']] - if options['router'] != DEFAULT_DB_ALIAS: - warnings.warn("--router is deprecated. You should use --database.", RemovedInNextVersionWarning, stacklevel=2) - databases = [options['router']] + databases = [options["database"]] + if options["router"] != DEFAULT_DB_ALIAS: + warnings.warn( + "--router is deprecated. You should use --database.", + RemovedInNextVersionWarning, + stacklevel=2, + ) + databases = [options["router"]] for i, database in enumerate(databases): if i != 0: @@ -67,90 +165,48 @@ def handle(self, *args, **options): def show_dsn(self, database, options): dbinfo = settings.DATABASES.get(database) - quiet = options['quiet'] - dsn_style = options['style'] + quiet = options["quiet"] + dsn_style = options["style"] if dbinfo is None: raise CommandError("Unknown database %s" % database) - engine = dbinfo.get('ENGINE') - dbuser = dbinfo.get('USER') - dbpass = dbinfo.get('PASSWORD') - dbname = dbinfo.get('NAME') - dbhost = dbinfo.get('HOST') - dbport = dbinfo.get('PORT') - - dsn = [] - - if engine in SQLITE_ENGINES: - dsn.append('{}'.format(dbname)) - elif engine in MYSQL_ENGINES: - dsn.append(self._mysql(dbhost, dbport, dbname, dbuser, dbpass)) - elif engine in POSTGRESQL_ENGINES: - dsn.extend(self._postgresql( - dbhost, dbport, dbname, dbuser, dbpass, dsn_style=dsn_style)) - else: - dsn.append(self.style.ERROR('Unknown database, can''t generate DSN')) + engine = dbinfo.get("ENGINE") + dbuser = dbinfo.get("USER") + dbpass = dbinfo.get("PASSWORD") + dbname = dbinfo.get("NAME") + dbhost = dbinfo.get("HOST") + dbport = dbinfo.get("PORT") + if dbport == "": + dbport = None + + dsn = [ + formatter(dbhost, dbport, dbname, dbuser, dbpass) + for engines, style, formatter in _FORMATTERS + if engine in engines + and (dsn_style == style or dsn_style == "all" and style is not None) + ] + + if not dsn: + available = ", ".join( + style + for engines, style, _ in _FORMATTERS + if engine in engines and style is not None + ) + dsn = [ + self.style.ERROR( + f"Invalid style {dsn_style} for {engine} (available: {available})" + if available + else "Unknown database, can't generate DSN" + ) + ] if not quiet: - sys.stdout.write(self.style.SQL_TABLE("DSN for database '%s' with engine '%s':\n" % (database, engine))) + sys.stdout.write( + self.style.SQL_TABLE( + f"DSN for database {database!r} with engine {engine!r}:\n" + ) + ) for output in dsn: - sys.stdout.write("{}\n".format(output)) - - def _mysql(self, dbhost, dbport, dbname, dbuser, dbpass): - dsnstr = 'host="{0}", db="{2}", user="{3}", passwd="{4}"' - - if dbport is not None: - dsnstr += ', port="{1}"' - - return dsnstr.format(dbhost, dbport, dbname, dbuser, dbpass) - - def _postgresql(self, dbhost, dbport, dbname, dbuser, dbpass, dsn_style=None): # noqa - """PostgreSQL psycopg2 driver accepts two syntaxes - - Plus a string for .pgpass file - """ - dsn = [] - - if dsn_style is None or dsn_style == 'all' or dsn_style == 'keyvalue': - dsnstr = "host='{0}' dbname='{2}' user='{3}' password='{4}'" - - if dbport is not None: - dsnstr += " port='{1}'" - - dsn.append(dsnstr.format( - dbhost, - dbport, - dbname, - dbuser, - dbpass, - )) - - if dsn_style in ('all', 'kwargs'): - dsnstr = "host='{0}', database='{2}', user='{3}', password='{4}'" - if dbport is not None: - dsnstr += ", port='{1}'" - - dsn.append(dsnstr.format( - dbhost, - dbport, - dbname, - dbuser, - dbpass, - )) - - if dsn_style in ('all', 'uri'): - dsnstr = "postgresql://{user}:{password}@{host}/{name}" - - dsn.append(dsnstr.format( - host="{host}:{port}".format(host=dbhost, port=dbport) if dbport else dbhost, # noqa - name=dbname, - user=dbuser, - password=dbpass, - )) - - if dsn_style in ('all', 'pgpass'): - dsn.append(':'.join(map(str, filter(None, [dbhost, dbport, dbname, dbuser, dbpass])))) - - return dsn + sys.stdout.write(f"{output}\n") diff --git a/django_extensions/management/commands/sync_s3.py b/django_extensions/management/commands/sync_s3.py index 251749e84..e591302a4 100644 --- a/django_extensions/management/commands/sync_s3.py +++ b/django_extensions/management/commands/sync_s3.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Sync Media to S3 ================ @@ -56,6 +55,7 @@ * Use fnmatch (or regex) to allow more complex FILTER_LIST rules. """ + import datetime import email import gzip @@ -81,148 +81,189 @@ class Command(BaseCommand): # Extra variables to avoid passing these around - AWS_ACCESS_KEY_ID = '' - AWS_SECRET_ACCESS_KEY = '' - AWS_BUCKET_NAME = '' - AWS_CLOUDFRONT_DISTRIBUTION = '' - SYNC_S3_RENAME_GZIP_EXT = '' - - DIRECTORIES = '' - FILTER_LIST = ['.DS_Store', '.svn', '.hg', '.git', 'Thumbs.db'] + AWS_ACCESS_KEY_ID = "" + AWS_SECRET_ACCESS_KEY = "" + AWS_BUCKET_NAME = "" + AWS_CLOUDFRONT_DISTRIBUTION = "" + SYNC_S3_RENAME_GZIP_EXT = "" + + DIRECTORIES = "" + FILTER_LIST = [".DS_Store", ".svn", ".hg", ".git", "Thumbs.db"] GZIP_CONTENT_TYPES = ( - 'text/css', - 'application/javascript', - 'application/x-javascript', - 'text/javascript' + "text/css", + "application/javascript", + "application/x-javascript", + "text/javascript", ) uploaded_files = [] # type: List[str] upload_count = 0 skip_count = 0 - help = 'Syncs the complete MEDIA_ROOT structure and files to S3 into the given bucket name.' - args = 'bucket_name' + help = "Syncs the complete MEDIA_ROOT structure and files to S3 into the given bucket name." # noqa: E501 + args = "bucket_name" can_import_settings = True def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '-p', '--prefix', - dest='prefix', - default=getattr(settings, 'SYNC_S3_PREFIX', ''), - help="The prefix to prepend to the path on S3." + "-p", + "--prefix", + dest="prefix", + default=getattr(settings, "SYNC_S3_PREFIX", ""), + help="The prefix to prepend to the path on S3.", ) parser.add_argument( - '-d', '--dir', - dest='dir', - help="Custom static root directory to use" + "-d", "--dir", dest="dir", help="Custom static root directory to use" ) parser.add_argument( - '--s3host', - dest='s3host', - default=getattr(settings, 'AWS_S3_HOST', ''), - help="The s3 host (enables connecting to other providers/regions)" + "--s3host", + dest="s3host", + default=getattr(settings, "AWS_S3_HOST", ""), + help="The s3 host (enables connecting to other providers/regions)", ) parser.add_argument( - '--acl', - dest='acl', - default=getattr(settings, 'AWS_DEFAULT_ACL', 'public-read'), - help="Enables to override default acl (public-read)." + "--acl", + dest="acl", + default=getattr(settings, "AWS_DEFAULT_ACL", "public-read"), + help="Enables to override default acl (public-read).", ) parser.add_argument( - '--gzip', - action='store_true', dest='gzip', default=False, - help="Enables gzipping CSS and Javascript files." + "--gzip", + action="store_true", + dest="gzip", + default=False, + help="Enables gzipping CSS and Javascript files.", ) parser.add_argument( - '--renamegzip', - action='store_true', dest='renamegzip', default=False, - help="Enables renaming of gzipped assets to have '.gz' appended to the filename." + "--renamegzip", + action="store_true", + dest="renamegzip", + default=False, + help=( + "Enables renaming of gzipped assets to have '.gz' " + "appended to the filename." + ), ) parser.add_argument( - '--expires', - action='store_true', dest='expires', default=False, - help="Enables setting a far future expires header." + "--expires", + action="store_true", + dest="expires", + default=False, + help="Enables setting a far future expires header.", ) parser.add_argument( - '--force', - action='store_true', dest='force', default=False, - help="Skip the file mtime check to force upload of all files." + "--force", + action="store_true", + dest="force", + default=False, + help="Skip the file mtime check to force upload of all files.", ) parser.add_argument( - '--filter-list', dest='filter_list', - action='store', default='', - help="Override default directory and file exclusion filters. (enter as comma seperated line)" + "--filter-list", + dest="filter_list", + action="store", + default="", + help=( + "Override default directory and file exclusion filters. " + "(enter as comma separated line)" + ), ) parser.add_argument( - '--invalidate', dest='invalidate', default=False, - action='store_true', - help='Invalidates the associated objects in CloudFront' + "--invalidate", + dest="invalidate", + default=False, + action="store_true", + help="Invalidates the associated objects in CloudFront", ) parser.add_argument( - '--media-only', dest='media_only', default='', - action='store_true', - help="Only MEDIA_ROOT files will be uploaded to S3" + "--media-only", + dest="media_only", + default="", + action="store_true", + help="Only MEDIA_ROOT files will be uploaded to S3", ) parser.add_argument( - '--static-only', dest='static_only', default='', - action='store_true', - help="Only STATIC_ROOT files will be uploaded to S3" + "--static-only", + dest="static_only", + default="", + action="store_true", + help="Only STATIC_ROOT files will be uploaded to S3", ) @signalcommand def handle(self, *args, **options): if not HAS_BOTO: - raise CommandError("Please install the 'boto' Python library. ($ pip install boto)") + raise CommandError( + "Please install the 'boto' Python library. ($ pip install boto)" + ) # Check for AWS keys in settings - if not hasattr(settings, 'AWS_ACCESS_KEY_ID') or not hasattr(settings, 'AWS_SECRET_ACCESS_KEY'): - raise CommandError('Missing AWS keys from settings file. Please supply both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.') + if not hasattr(settings, "AWS_ACCESS_KEY_ID") or not hasattr( + settings, "AWS_SECRET_ACCESS_KEY" + ): + raise CommandError( + ( + "Missing AWS keys from settings file. Please supply both " + "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY." + ) + ) else: self.AWS_ACCESS_KEY_ID = settings.AWS_ACCESS_KEY_ID self.AWS_SECRET_ACCESS_KEY = settings.AWS_SECRET_ACCESS_KEY - if not hasattr(settings, 'AWS_BUCKET_NAME'): - raise CommandError('Missing bucket name from settings file. Please add the AWS_BUCKET_NAME to your settings file.') + if not hasattr(settings, "AWS_BUCKET_NAME"): + raise CommandError( + ( + "Missing bucket name from settings file. Please add the " + "AWS_BUCKET_NAME to your settings file." + ) + ) else: if not settings.AWS_BUCKET_NAME: - raise CommandError('AWS_BUCKET_NAME cannot be empty.') + raise CommandError("AWS_BUCKET_NAME cannot be empty.") self.AWS_BUCKET_NAME = settings.AWS_BUCKET_NAME - if not hasattr(settings, 'MEDIA_ROOT'): - raise CommandError('MEDIA_ROOT must be set in your settings.') + if not hasattr(settings, "MEDIA_ROOT"): + raise CommandError("MEDIA_ROOT must be set in your settings.") else: if not settings.MEDIA_ROOT: - raise CommandError('MEDIA_ROOT must be set in your settings.') + raise CommandError("MEDIA_ROOT must be set in your settings.") - self.AWS_CLOUDFRONT_DISTRIBUTION = getattr(settings, 'AWS_CLOUDFRONT_DISTRIBUTION', '') + self.AWS_CLOUDFRONT_DISTRIBUTION = getattr( + settings, "AWS_CLOUDFRONT_DISTRIBUTION", "" + ) - self.SYNC_S3_RENAME_GZIP_EXT = \ - getattr(settings, 'SYNC_S3_RENAME_GZIP_EXT', '.gz') + self.SYNC_S3_RENAME_GZIP_EXT = getattr( + settings, "SYNC_S3_RENAME_GZIP_EXT", ".gz" + ) self.verbosity = options["verbosity"] - self.prefix = options['prefix'] - self.do_gzip = options['gzip'] - self.rename_gzip = options['renamegzip'] - self.do_expires = options['expires'] - self.do_force = options['force'] - self.invalidate = options['invalidate'] - self.DIRECTORIES = options['dir'] - self.s3host = options['s3host'] - self.default_acl = options['acl'] - self.FILTER_LIST = getattr(settings, 'FILTER_LIST', self.FILTER_LIST) - filter_list = options['filter_list'] + self.prefix = options["prefix"] + self.do_gzip = options["gzip"] + self.rename_gzip = options["renamegzip"] + self.do_expires = options["expires"] + self.do_force = options["force"] + self.invalidate = options["invalidate"] + self.DIRECTORIES = options["dir"] + self.s3host = options["s3host"] + self.default_acl = options["acl"] + self.FILTER_LIST = getattr(settings, "FILTER_LIST", self.FILTER_LIST) + filter_list = options["filter_list"] if filter_list: # command line option overrides default filter_list and # settings.filter_list - self.FILTER_LIST = filter_list.split(',') + self.FILTER_LIST = filter_list.split(",") - self.media_only = options['media_only'] - self.static_only = options['static_only'] + self.media_only = options["media_only"] + self.static_only = options["static_only"] # Get directories if self.media_only and self.static_only: - raise CommandError("Can't use --media-only and --static-only together. Better not use anything...") + raise CommandError( + "Can't use --media-only and --static-only together. " + "Better not use anything..." + ) elif self.media_only: self.DIRECTORIES = [settings.MEDIA_ROOT] elif self.static_only: @@ -248,14 +289,16 @@ def handle(self, *args, **options): def open_cf(self): """Return an open connection to CloudFront""" return boto.connect_cloudfront( - self.AWS_ACCESS_KEY_ID, self.AWS_SECRET_ACCESS_KEY) + self.AWS_ACCESS_KEY_ID, self.AWS_SECRET_ACCESS_KEY + ) def invalidate_objects_cf(self): """Split the invalidation request in groups of 1000 objects""" if not self.AWS_CLOUDFRONT_DISTRIBUTION: raise CommandError( - 'An object invalidation was requested but the variable ' - 'AWS_CLOUDFRONT_DISTRIBUTION is not present in your settings.') + "An object invalidation was requested but the variable " + "AWS_CLOUDFRONT_DISTRIBUTION is not present in your settings." + ) # We can't send more than 1000 objects in the same invalidation # request. @@ -266,24 +309,25 @@ def invalidate_objects_cf(self): # Splitting the object list objs = self.uploaded_files - chunks = [objs[i:i + chunk] for i in range(0, len(objs), chunk)] + chunks = [objs[i : i + chunk] for i in range(0, len(objs), chunk)] # Invalidation requests for paths in chunks: - conn.create_invalidation_request( - self.AWS_CLOUDFRONT_DISTRIBUTION, paths) + conn.create_invalidation_request(self.AWS_CLOUDFRONT_DISTRIBUTION, paths) def sync_s3(self): """Walk the media/static directories and syncs files to S3""" bucket, key = self.open_s3() for directory in self.DIRECTORIES: for root, dirs, files in os.walk(directory): - self.upload_s3((bucket, key, self.AWS_BUCKET_NAME, directory), root, files, dirs) + self.upload_s3( + (bucket, key, self.AWS_BUCKET_NAME, directory), root, files, dirs + ) def compress_string(self, s): """Gzip a given string.""" zbuf = StringIO() - zfile = gzip.GzipFile(mode='wb', compresslevel=6, fileobj=zbuf) + zfile = gzip.GzipFile(mode="wb", compresslevel=6, fileobj=zbuf) zfile.write(s) zfile.close() return zbuf.getvalue() @@ -292,7 +336,7 @@ def get_s3connection_kwargs(self): """Return connection kwargs as a dict""" kwargs = {} if self.s3host: - kwargs['host'] = self.s3host + kwargs["host"] = self.s3host return kwargs def open_s3(self): @@ -300,7 +344,8 @@ def open_s3(self): conn = boto.connect_s3( self.AWS_ACCESS_KEY_ID, self.AWS_SECRET_ACCESS_KEY, - **self.get_s3connection_kwargs()) + **self.get_s3connection_kwargs(), + ) try: bucket = conn.get_bucket(self.AWS_BUCKET_NAME) except boto.exception.S3ResponseError: @@ -311,7 +356,10 @@ def upload_s3(self, arg, dirname, names, dirs): bucket, key, bucket_name, root_dir = arg # Skip directories we don't want to sync - if os.path.basename(dirname) in self.FILTER_LIST and os.path.dirname(dirname) in self.DIRECTORIES: + if ( + os.path.basename(dirname) in self.FILTER_LIST + and os.path.dirname(dirname) in self.DIRECTORIES + ): # prevent walk from processing subfiles/subdirs below the ignored one del dirs[:] return @@ -330,22 +378,29 @@ def upload_s3(self, arg, dirname, names, dirs): if os.path.isdir(filename): continue # Don't try to upload directories - file_key = filename[len(root_dir):] + file_key = filename[len(root_dir) :] if self.prefix: - file_key = '%s/%s' % (self.prefix, file_key) + file_key = "%s/%s" % (self.prefix, file_key) # Check if file on S3 is older than local file, if so, upload if not self.do_force: s3_key = bucket.get_key(file_key) if s3_key: - s3_datetime = datetime.datetime(*time.strptime( - s3_key.last_modified, '%a, %d %b %Y %H:%M:%S %Z')[0:6]) + s3_datetime = datetime.datetime( + *time.strptime( + s3_key.last_modified, "%a, %d %b %Y %H:%M:%S %Z" + )[0:6] + ) local_datetime = datetime.datetime.utcfromtimestamp( - os.stat(filename).st_mtime) + os.stat(filename).st_mtime + ) if local_datetime < s3_datetime: self.skip_count += 1 if self.verbosity > 1: - print("File %s hasn't been modified since last being uploaded" % file_key) + print( + "File %s hasn't been modified since last being uploaded" + % file_key + ) continue # File is newer, let's process and upload @@ -354,11 +409,11 @@ def upload_s3(self, arg, dirname, names, dirs): content_type = mimetypes.guess_type(filename)[0] if content_type: - headers['Content-Type'] = content_type + headers["Content-Type"] = content_type else: - headers['Content-Type'] = 'application/octet-stream' + headers["Content-Type"] = "application/octet-stream" - file_obj = open(filename, 'rb') + file_obj = open(filename, "rb") file_size = os.fstat(file_obj.fileno()).st_size filedata = file_obj.read() if self.do_gzip: @@ -370,24 +425,36 @@ def upload_s3(self, arg, dirname, names, dirs): # If rename_gzip is True, then rename the file # by appending an extension (like '.gz)' to # original filename. - file_key = '%s.%s' % ( - file_key, self.SYNC_S3_RENAME_GZIP_EXT) - headers['Content-Encoding'] = 'gzip' + file_key = "%s.%s" % (file_key, self.SYNC_S3_RENAME_GZIP_EXT) + headers["Content-Encoding"] = "gzip" if self.verbosity > 1: - print("\tgzipped: %dk to %dk" % (file_size / 1024, len(filedata) / 1024)) + print( + "\tgzipped: %dk to %dk" + % (file_size / 1024, len(filedata) / 1024) + ) if self.do_expires: # HTTP/1.0 - headers['Expires'] = '%s GMT' % (email.Utils.formatdate(time.mktime((datetime.datetime.now() + datetime.timedelta(days=365 * 2)).timetuple()))) + headers["Expires"] = "%s GMT" % ( + email.Utils.formatdate( + time.mktime( + ( + datetime.datetime.now() + + datetime.timedelta(days=365 * 2) + ).timetuple() + ) + ) + ) # HTTP/1.1 - headers['Cache-Control'] = 'max-age %d' % (3600 * 24 * 365 * 2) + headers["Cache-Control"] = "max-age %d" % (3600 * 24 * 365 * 2) if self.verbosity > 1: - print("\texpires: %s" % headers['Expires']) - print("\tcache-control: %s" % headers['Cache-Control']) + print("\texpires: %s" % headers["Expires"]) + print("\tcache-control: %s" % headers["Cache-Control"]) try: key.name = file_key - key.set_contents_from_string(filedata, headers, replace=True, - policy=self.default_acl) + key.set_contents_from_string( + filedata, headers, replace=True, policy=self.default_acl + ) except boto.exception.S3CreateError as e: print("Failed: %s" % e) except Exception as e: diff --git a/django_extensions/management/commands/syncdata.py b/django_extensions/management/commands/syncdata.py index c05806b31..57b4bbba8 100644 --- a/django_extensions/management/commands/syncdata.py +++ b/django_extensions/management/commands/syncdata.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ SyncData ======== @@ -23,7 +22,7 @@ def humanize(dirname): - return "'%s'" % dirname if dirname else 'absolute path' + return "'%s'" % dirname if dirname else "absolute path" class SyncDataError(Exception): @@ -31,28 +30,40 @@ class SyncDataError(Exception): class Command(BaseCommand): - """ syncdata command """ + """syncdata command""" - help = 'Makes the current database have the same data as the fixture(s), no more, no less.' + help = "Makes the current database have the same data as the fixture(s), no more, no less." # noqa: E501 args = "fixture [fixture ...]" def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--skip-remove', action='store_false', dest='remove', default=True, - help='Avoid remove any object from db', + "--skip-remove", + action="store_false", + dest="remove", + default=True, + help="Avoid remove any object from db", ) parser.add_argument( - '--remove-before', action='store_true', dest='remove_before', default=False, - help='Remove existing objects before inserting and updating new ones', + "--remove-before", + action="store_true", + dest="remove_before", + default=False, + help="Remove existing objects before inserting and updating new ones", ) parser.add_argument( - '--database', default=DEFAULT_DB_ALIAS, - help='Nominates a specific database to load fixtures into. Defaults to the "default" database.', + "--database", + default=DEFAULT_DB_ALIAS, + help=( + "Nominates a specific database to load fixtures into. " + 'Defaults to the "default" database.' + ), ) parser.add_argument( - 'fixture_labels', nargs='?', type=str, - help='Specify the fixture label (comma separated)', + "fixture_labels", + nargs="?", + type=str, + help="Specify the fixture label (comma separated)", ) def remove_objects_not_in(self, objects_to_keep, verbosity): @@ -86,8 +97,10 @@ def remove_objects_not_in(self, objects_to_keep, verbosity): @signalcommand def handle(self, *args, **options): self.style = no_style() - self.using = options['database'] - fixture_labels = options['fixture_labels'].split(',') if options['fixture_labels'] else () + self.using = options["database"] + fixture_labels = ( + options["fixture_labels"].split(",") if options["fixture_labels"] else () + ) try: with transaction.atomic(): self.syncdata(fixture_labels, options) @@ -102,8 +115,8 @@ def handle(self, *args, **options): connections[self.using].close() def syncdata(self, fixture_labels, options): - verbosity = options['verbosity'] - show_traceback = options['traceback'] + verbosity = options["verbosity"] + show_traceback = options["traceback"] # Keep a count of the installed objects and fixtures fixture_count = 0 @@ -117,14 +130,17 @@ def syncdata(self, fixture_labels, options): cursor = connections[self.using].cursor() app_modules = [app.module for app in apps.get_app_configs()] - app_fixtures = [os.path.join(os.path.dirname(app.__file__), 'fixtures') for app in app_modules] + app_fixtures = [ + os.path.join(os.path.dirname(app.__file__), "fixtures") + for app in app_modules + ] for fixture_label in fixture_labels: - parts = fixture_label.split('.') + parts = fixture_label.split(".") if len(parts) == 1: fixture_name = fixture_label formats = serializers.get_public_serializer_formats() else: - fixture_name, format_ = '.'.join(parts[:-1]), parts[-1] + fixture_name, format_ = ".".join(parts[:-1]), parts[-1] if format_ in serializers.get_public_serializer_formats(): formats = [format_] else: @@ -134,12 +150,18 @@ def syncdata(self, fixture_labels, options): if verbosity > 1: print("Loading '%s' fixtures..." % fixture_name) else: - raise SyncDataError("Problem installing fixture '%s': %s is not a known serialization format." % (fixture_name, format_)) + raise SyncDataError( + ( + "Problem installing fixture '%s': %s is not a known " + "serialization format." + ) + % (fixture_name, format_) + ) if os.path.isabs(fixture_name): fixture_dirs = [fixture_name] else: - fixture_dirs = app_fixtures + list(settings.FIXTURE_DIRS) + [''] + fixture_dirs = app_fixtures + list(settings.FIXTURE_DIRS) + [""] for fixture_dir in fixture_dirs: if verbosity > 1: @@ -148,29 +170,44 @@ def syncdata(self, fixture_labels, options): label_found = False for format_ in formats: if verbosity > 1: - print("Trying %s for %s fixture '%s'..." % (humanize(fixture_dir), format_, fixture_name)) + print( + "Trying %s for %s fixture '%s'..." + % (humanize(fixture_dir), format_, fixture_name) + ) try: - full_path = os.path.join(fixture_dir, '.'.join([fixture_name, format_])) - fixture = open(full_path, 'r') + full_path = os.path.join( + fixture_dir, ".".join([fixture_name, format_]) + ) + fixture = open(full_path, "r") if label_found: fixture.close() - raise SyncDataError("Multiple fixtures named '%s' in %s. Aborting." % (fixture_name, humanize(fixture_dir))) + raise SyncDataError( + "Multiple fixtures named '%s' in %s. Aborting." + % (fixture_name, humanize(fixture_dir)) + ) else: fixture_count += 1 objects_per_fixture.append(0) if verbosity > 0: - print("Installing %s fixture '%s' from %s." % (format_, fixture_name, humanize(fixture_dir))) + print( + "Installing %s fixture '%s' from %s." + % (format_, fixture_name, humanize(fixture_dir)) + ) try: objects_to_keep = {} - objects = list(serializers.deserialize(format_, fixture)) + objects = list( + serializers.deserialize(format_, fixture) + ) for obj in objects: class_ = obj.object.__class__ if class_ not in objects_to_keep: objects_to_keep[class_] = set() objects_to_keep[class_].add(obj.object) - if options['remove'] and options['remove_before']: - self.remove_objects_not_in(objects_to_keep, verbosity) + if options["remove"] and options["remove_before"]: + self.remove_objects_not_in( + objects_to_keep, verbosity + ) for obj in objects: object_count += 1 @@ -178,35 +215,49 @@ def syncdata(self, fixture_labels, options): models.add(obj.object.__class__) obj.save() - if options['remove'] and not options['remove_before']: - self.remove_objects_not_in(objects_to_keep, verbosity) + if options["remove"] and not options["remove_before"]: + self.remove_objects_not_in( + objects_to_keep, verbosity + ) label_found = True except (SystemExit, KeyboardInterrupt): raise except Exception: import traceback + fixture.close() if show_traceback: traceback.print_exc() - raise SyncDataError("Problem installing fixture '%s': %s\n" % (full_path, traceback.format_exc())) + raise SyncDataError( + "Problem installing fixture '%s': %s\n" + % (full_path, traceback.format_exc()) + ) fixture.close() except SyncDataError as e: raise e except Exception: if verbosity > 1: - print("No %s fixture '%s' in %s." % (format_, fixture_name, humanize(fixture_dir))) + print( + "No %s fixture '%s' in %s." + % (format_, fixture_name, humanize(fixture_dir)) + ) # If any of the fixtures we loaded contain 0 objects, assume that an # error was encountered during fixture loading. if 0 in objects_per_fixture: - raise SyncDataError("No fixture data found for '%s'. (File format may be invalid.)" % fixture_name) + raise SyncDataError( + "No fixture data found for '%s'. (File format may be invalid.)" + % fixture_name + ) # If we found even one object in a fixture, we need to reset the # database sequences. if object_count > 0: - sequence_sql = connections[self.using].ops.sequence_reset_sql(self.style, models) + sequence_sql = connections[self.using].ops.sequence_reset_sql( + self.style, models + ) if sequence_sql: if verbosity > 1: print("Resetting sequences") @@ -218,7 +269,12 @@ def syncdata(self, fixture_labels, options): print("No fixtures found.") else: if verbosity > 0: - print("Installed %d object%s from %d fixture%s" % ( - object_count, pluralize(object_count), - fixture_count, pluralize(fixture_count) - )) + print( + "Installed %d object%s from %d fixture%s" + % ( + object_count, + pluralize(object_count), + fixture_count, + pluralize(fixture_count), + ) + ) diff --git a/django_extensions/management/commands/unreferenced_files.py b/django_extensions/management/commands/unreferenced_files.py index abedf7d68..efe37f1fe 100644 --- a/django_extensions/management/commands/unreferenced_files.py +++ b/django_extensions/management/commands/unreferenced_files.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os from collections import defaultdict @@ -11,11 +10,11 @@ class Command(BaseCommand): - help = "Prints a list of all files in MEDIA_ROOT that are not referenced in the database." + help = "Prints a list of all files in MEDIA_ROOT that are not referenced in the database." # noqa: E501 @signalcommand def handle(self, *args, **options): - if not getattr(settings, 'MEDIA_ROOT'): + if not getattr(settings, "MEDIA_ROOT"): raise CommandError("MEDIA_ROOT is not set, nothing to do") # Get a list of all files under MEDIA_ROOT diff --git a/django_extensions/management/commands/update_permissions.py b/django_extensions/management/commands/update_permissions.py index 6b0e4cce5..9ea86ba2e 100644 --- a/django_extensions/management/commands/update_permissions.py +++ b/django_extensions/management/commands/update_permissions.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from django import VERSION as DJANGO_VERSION from django.apps import apps as django_apps -from django.contrib.auth.management import create_permissions, _get_all_permissions +from django.contrib.auth.management import create_permissions +from django.contrib.auth.management import _get_all_permissions # type: ignore from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand @@ -10,41 +9,49 @@ class Command(BaseCommand): - help = 'reloads permissions for specified apps, or all apps if no args are specified' + help = ( + "reloads permissions for specified apps, or all apps if no args are specified" + ) def add_arguments(self, parser): super().add_arguments(parser) - parser.add_argument('--apps', dest='apps', help='Reload permissions only for apps (comma separated)') - parser.add_argument('--create-only', action='store_true', default=False, help='Only create missing permissions') - parser.add_argument('--update-only', action='store_true', default=False, help='Only update permissions') + parser.add_argument( + "--apps", + dest="apps", + help="Reload permissions only for apps (comma separated)", + ) + parser.add_argument( + "--create-only", + action="store_true", + default=False, + help="Only create missing permissions", + ) + parser.add_argument( + "--update-only", + action="store_true", + default=False, + help="Only update permissions", + ) @signalcommand def handle(self, *args, **options): - if options['apps']: - app_names = options['apps'].split(',') + if options["apps"]: + app_names = options["apps"].split(",") apps = [django_apps.get_app_config(x) for x in app_names] else: apps = django_apps.get_app_configs() - if options['create_only']: + if options["create_only"]: do_create, do_update = True, False - elif options['update_only']: + elif options["update_only"]: do_create, do_update = False, True else: do_create, do_update = True, True for app in apps: - if DJANGO_VERSION < (2, 2): - # see https://github.com/django/django/commit/bec651a427fc032d9115d30c8c5d0e702d754f6c - # Ensure that contenttypes are created for this app. Needed if - # 'django.contrib.auth' is in INSTALLED_APPS before - # 'django.contrib.contenttypes'. - from django.contrib.contenttypes.management import create_contenttypes - create_contenttypes(app, verbosity=options['verbosity']) - if do_create: # create permissions if they do not exist - create_permissions(app, options['verbosity']) + create_permissions(app, options["verbosity"]) if do_update: # update permission name's if changed @@ -52,12 +59,19 @@ def handle(self, *args, **options): content_type = ContentType.objects.get_for_model(model) for codename, name in _get_all_permissions(model._meta): try: - permission = Permission.objects.get(codename=codename, content_type=content_type) + permission = Permission.objects.get( + codename=codename, content_type=content_type + ) except Permission.DoesNotExist: continue if permission.name != name: old_str = str(permission) permission.name = name - if options['verbosity'] >= 2: - self.stdout.write(self.style.SUCCESS("Update permission '%s' to '%s'" % (old_str, permission))) + if options["verbosity"] >= 2: + self.stdout.write( + self.style.SUCCESS( + "Update permission '%s' to '%s'" + % (old_str, permission) + ) + ) permission.save() diff --git a/django_extensions/management/commands/validate_templates.py b/django_extensions/management/commands/validate_templates.py index 1810298c7..33b6a503c 100644 --- a/django_extensions/management/commands/validate_templates.py +++ b/django_extensions/management/commands/validate_templates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import os import fnmatch @@ -18,28 +17,48 @@ class Command(BaseCommand): - args = '' + args = "" help = "Validate templates on syntax and compile errors" - ignores = set([ - ".DS_Store", - "*.swp", - "*~", - ]) + ignores = set( + [ + ".DS_Store", + "*.swp", + "*~", + ] + ) def add_arguments(self, parser): super().add_arguments(parser) parser.add_argument( - '--no-apps', action='store_true', dest='no_apps', - default=False, help="Do not automatically include apps.") + "--no-apps", + action="store_true", + dest="no_apps", + default=False, + help="Do not automatically include apps.", + ) parser.add_argument( - '--break', '-b', action='store_true', dest='break', - default=False, help="Break on first error.") + "--break", + "-b", + action="store_true", + dest="break", + default=False, + help="Break on first error.", + ) parser.add_argument( - '--include', '-i', action='append', dest='includes', - default=[], help="Append these paths to TEMPLATE DIRS") + "--include", + "-i", + action="append", + dest="includes", + default=[], + help="Append these paths to TEMPLATE DIRS", + ) parser.add_argument( - '--ignore-app', action='append', dest='ignore_apps', - default=[], help="Ignore these apps") + "--ignore-app", + action="append", + dest="ignore_apps", + default=[], + help="Ignore these apps", + ) def ignore_filename(self, filename): filename = os.path.basename(filename) @@ -50,26 +69,28 @@ def ignore_filename(self, filename): @signalcommand def handle(self, *args, **options): - if hasattr(settings, 'VALIDATE_TEMPLATES_IGNORES'): - self.ignores = getattr(settings, 'VALIDATE_TEMPLATES_IGNORES') + if hasattr(settings, "VALIDATE_TEMPLATES_IGNORES"): + self.ignores = getattr(settings, "VALIDATE_TEMPLATES_IGNORES") style = color_style() - template_dirs = set(get_template_setting('DIRS', [])) - template_dirs |= set(options['includes']) - template_dirs |= set(getattr(settings, 'VALIDATE_TEMPLATES_EXTRA_TEMPLATE_DIRS', [])) + template_dirs = set(get_template_setting("DIRS", [])) + template_dirs |= set(options["includes"]) + template_dirs |= set( + getattr(settings, "VALIDATE_TEMPLATES_EXTRA_TEMPLATE_DIRS", []) + ) - if not options['no_apps']: - ignore_apps = options['ignore_apps'] - if not ignore_apps and hasattr(settings, 'VALIDATE_TEMPLATES_IGNORE_APPS'): - ignore_apps = getattr(settings, 'VALIDATE_TEMPLATES_IGNORE_APPS') + if not options["no_apps"]: + ignore_apps = options["ignore_apps"] + if not ignore_apps and hasattr(settings, "VALIDATE_TEMPLATES_IGNORE_APPS"): + ignore_apps = getattr(settings, "VALIDATE_TEMPLATES_IGNORE_APPS") for app in apps.get_app_configs(): if app.name in ignore_apps: continue - app_template_dir = os.path.join(app.path, 'templates') + app_template_dir = os.path.join(app.path, "templates") if os.path.isdir(app_template_dir): template_dirs.add(app_template_dir) - settings.TEMPLATES[0]['DIRS'] = list(template_dirs) + settings.TEMPLATES[0]["DIRS"] = list(template_dirs) settings.TEMPLATE_DEBUG = True verbosity = options["verbosity"] errors = 0 @@ -87,8 +108,14 @@ def handle(self, *args, **options): get_template(filepath) except Exception as e: errors += 1 - self.stdout.write("%s: %s" % (filepath, style.ERROR("%s %s" % (e.__class__.__name__, str(e))))) - if errors and options['break']: + self.stdout.write( + "%s: %s" + % ( + filepath, + style.ERROR("%s %s" % (e.__class__.__name__, str(e))), + ) + ) + if errors and options["break"]: raise CommandError("Errors found") if errors: diff --git a/django_extensions/management/commands/verify_named_urls.py b/django_extensions/management/commands/verify_named_urls.py new file mode 100644 index 000000000..973cc842a --- /dev/null +++ b/django_extensions/management/commands/verify_named_urls.py @@ -0,0 +1,236 @@ +from collections import defaultdict +import fnmatch +import functools +import re +import os + +from django.apps import apps +from django.conf import settings +from django.core.exceptions import ViewDoesNotExist +from django.core.management.base import BaseCommand, CommandError +from django.template.loader import get_template +from django.urls import URLPattern, URLResolver +from django.utils import translation + +from django_extensions.compat import get_template_setting +from django_extensions.management.color import color_style, no_style +from django_extensions.management.utils import signalcommand + + +class RegexURLPattern: + pass + + +class RegexURLResolver: + pass + + +class LocaleRegexURLResolver: + pass + + +class Command(BaseCommand): + args = "" + help = "Verify named URLs in templates" + ignores = set( + [ + "*.swp", + "*~", + ] + ) + + def add_arguments(self, parser): + super().add_arguments(parser) + parser.add_argument( + "--ignore-app", + action="append", + dest="ignore_apps", + default=["admin"], + help="Ignore these apps", + ) + parser.add_argument( + "--urlconf", + "-c", + dest="urlconf", + default="ROOT_URLCONF", + help="Set the settings URL conf variable to use", + ) + + def ignore_filename(self, filename): + filename = os.path.basename(filename) + for ignore_pattern in self.ignores: + if fnmatch.fnmatch(filename, ignore_pattern): + return True + return False + + @signalcommand + def handle(self, *args, **options): + style = no_style() if options["no_color"] else color_style() + + self.names = defaultdict(list) + self.views = {} + + self.collect_templates(options) + self.collect_views(options) + + for name in sorted(self.names): + n = len(self.names[name]) + color = style.MODULE + try: + v = self.views[name] + print( + style.INFO( + f"Name: {name} ({n} occurences, handled in {v[0]}, {v[1]})" + ) + ) + except KeyError: + print(style.URL_NAME(f"Name: {name} ({n} occurences, UNKNOWN VIEW)")) + color = style.URL_NAME + for item in self.names[name]: + print(color(f"* {item[0]}:{item[1]}")) + + def collect_templates(self, options): + template_dirs = set(get_template_setting("DIRS", [])) + + for app in apps.get_app_configs(): + if app.name.split(".")[-1] in options["ignore_apps"]: + continue + app_template_dir = os.path.join(app.path, "templates") + if os.path.isdir(app_template_dir): + template_dirs.add(app_template_dir) + + settings.TEMPLATES[0]["DIRS"] = list(template_dirs) + + self.template_parse_errors = 0 + self.names_re = re.compile(r"\{%\s*url\s*['\"]([\w\-]+)['\"]") + + for template_dir in template_dirs: + for root, dirs, filenames in os.walk(template_dir): + for filename in filenames: + if self.ignore_filename(filename): + continue + filepath = os.path.join(root, filename) + self.process_template(filepath) + + if self.template_parse_errors > 0: + self.stdout.write( + f"{self.template_parse_errors} template parse errors found" + ) + + def collect_views(self, options): + urlconf = options["urlconf"] + + if not hasattr(settings, urlconf): + raise CommandError( + "Settings module {} does not have the attribute {}.".format( + settings, urlconf + ) + ) + + try: + urlconf = __import__(getattr(settings, urlconf), {}, {}, [""]) + except Exception as e: + raise CommandError( + "Error occurred while trying to load %s: %s" + % (getattr(settings, urlconf), str(e)) + ) + + view_functions = self.extract_views_from_urlpatterns(urlconf.urlpatterns) + for func, regex, view in view_functions: + if view is not None: + if isinstance(func, functools.partial): + func = func.func + if hasattr(func, "view_class"): + func = func.view_class + if hasattr(func, "__name__"): + func_name = func.__name__ + elif hasattr(func, "__class__"): + func_name = "%s()" % func.__class__.__name__ + else: + func_name = re.sub(r" at 0x[0-9a-f]+", "", repr(func)) + + self.views[view] = (func_name, regex) + + def process_template(self, filepath): + try: + get_template(filepath) + except Exception: + self.template_parse_errors += 1 + self.stdout.write(f"Error parsing template {filepath}") + + with open(filepath, "r") as file: + lineno = 1 + for line in file: + for match in self.names_re.findall(line): + self.names[match].append((filepath, lineno)) + lineno += 1 + + # copied from show_urls.py + def extract_views_from_urlpatterns(self, urlpatterns, base="", namespace=None): + """ + Return a list of views from a list of urlpatterns. + + Each object in the returned list is a three-tuple: (view_func, regex, name) + """ + views = [] + for p in urlpatterns: + if isinstance(p, (URLPattern, RegexURLPattern)): + try: + if not p.name: + name = p.name + elif namespace: + name = "{0}:{1}".format(namespace, p.name) + else: + name = p.name + pattern = describe_pattern(p) + views.append((p.callback, base + pattern, name)) + except ViewDoesNotExist: + continue + elif isinstance(p, (URLResolver, RegexURLResolver)): + try: + patterns = p.url_patterns + except ImportError: + continue + if namespace and p.namespace: + _namespace = "{0}:{1}".format(namespace, p.namespace) + else: + _namespace = p.namespace or namespace + pattern = describe_pattern(p) + if isinstance(p, LocaleRegexURLResolver): + for language in self.LANGUAGES: + with translation.override(language[0]): + views.extend( + self.extract_views_from_urlpatterns( + patterns, base + pattern, namespace=_namespace + ) + ) + else: + views.extend( + self.extract_views_from_urlpatterns( + patterns, base + pattern, namespace=_namespace + ) + ) + elif hasattr(p, "_get_callback"): + try: + views.append( + (p._get_callback(), base + describe_pattern(p), p.name) + ) + except ViewDoesNotExist: + continue + elif hasattr(p, "url_patterns") or hasattr(p, "_get_url_patterns"): + try: + patterns = p.url_patterns + except ImportError: + continue + views.extend( + self.extract_views_from_urlpatterns( + patterns, base + describe_pattern(p), namespace=namespace + ) + ) + else: + raise TypeError("%s does not appear to be a urlpattern object" % p) + return views + + +def describe_pattern(p): + return str(p.pattern) diff --git a/django_extensions/management/debug_cursor.py b/django_extensions/management/debug_cursor.py index ad68cfd6a..d04cd2e82 100644 --- a/django_extensions/management/debug_cursor.py +++ b/django_extensions/management/debug_cursor.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- import time import traceback from contextlib import contextmanager -import django from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.db.backends import utils @@ -12,16 +10,25 @@ @contextmanager -def monkey_patch_cursordebugwrapper(print_sql=None, print_sql_location=False, truncate=None, logger=print, confprefix="DJANGO_EXTENSIONS"): +def monkey_patch_cursordebugwrapper( + print_sql=None, + print_sql_location=False, + truncate=None, + logger=print, + confprefix="DJANGO_EXTENSIONS", +): if not print_sql: yield else: if truncate is None: - truncate = getattr(settings, '%s_PRINT_SQL_TRUNCATE' % confprefix, DEFAULT_PRINT_SQL_TRUNCATE_CHARS) + truncate = getattr( + settings, + "%s_PRINT_SQL_TRUNCATE" % confprefix, + DEFAULT_PRINT_SQL_TRUNCATE_CHARS, + ) - # Code orginally from http://gist.github.com/118990 sqlparse = None - if getattr(settings, '%s_SQLPARSE_ENABLED' % confprefix, True): + if getattr(settings, "%s_SQLPARSE_ENABLED" % confprefix, True): try: import sqlparse @@ -29,18 +36,28 @@ def monkey_patch_cursordebugwrapper(print_sql=None, print_sql_location=False, tr reindent_aligned=True, truncate_strings=500, ) - sqlparse_format_kwargs = getattr(settings, '%s_SQLPARSE_FORMAT_KWARGS' % confprefix, sqlparse_format_kwargs_defaults) + sqlparse_format_kwargs = getattr( + settings, + "%s_SQLPARSE_FORMAT_KWARGS" % confprefix, + sqlparse_format_kwargs_defaults, + ) except ImportError: sqlparse = None pygments = None - if getattr(settings, '%s_PYGMENTS_ENABLED' % confprefix, True): + if getattr(settings, "%s_PYGMENTS_ENABLED" % confprefix, True): try: import pygments.lexers import pygments.formatters - pygments_formatter = getattr(settings, '%s_PYGMENTS_FORMATTER' % confprefix, pygments.formatters.TerminalFormatter) - pygments_formatter_kwargs = getattr(settings, '%s_PYGMENTS_FORMATTER_KWARGS' % confprefix, {}) + pygments_formatter = getattr( + settings, + "%s_PYGMENTS_FORMATTER" % confprefix, + pygments.formatters.TerminalFormatter, + ) + pygments_formatter_kwargs = getattr( + settings, "%s_PYGMENTS_FORMATTER_KWARGS" % confprefix, {} + ) except ImportError: pass @@ -66,10 +83,13 @@ def execute(self, sql, params=()): ) logger(raw_sql) - logger("Execution time: %.6fs [Database: %s]" % (execution_time, self.db.alias)) + logger( + "Execution time: %.6fs [Database: %s]" + % (execution_time, self.db.alias) + ) if print_sql_location: logger("Location of SQL Call:") - logger(''.join(traceback.format_stack())) + logger("".join(traceback.format_stack())) _CursorDebugWrapper = utils.CursorDebugWrapper @@ -78,24 +98,29 @@ class PrintCursorQueryWrapper(PrintQueryWrapperMixin, _CursorDebugWrapper): try: from django.db import connections + _force_debug_cursor = {} for connection_name in connections: - _force_debug_cursor[connection_name] = connections[connection_name].force_debug_cursor + _force_debug_cursor[connection_name] = connections[ + connection_name + ].force_debug_cursor except Exception: connections = None utils.CursorDebugWrapper = PrintCursorQueryWrapper postgresql_base = None - if django.VERSION >= (3, 0): - try: - from django.db.backends.postgresql import base as postgresql_base - _PostgreSQLCursorDebugWrapper = postgresql_base.CursorDebugWrapper + try: + from django.db.backends.postgresql import base as postgresql_base - class PostgreSQLPrintCursorDebugWrapper(PrintQueryWrapperMixin, _PostgreSQLCursorDebugWrapper): - pass - except (ImproperlyConfigured, TypeError): - postgresql_base = None + _PostgreSQLCursorDebugWrapper = postgresql_base.CursorDebugWrapper + + class PostgreSQLPrintCursorDebugWrapper( + PrintQueryWrapperMixin, _PostgreSQLCursorDebugWrapper + ): + pass + except (ImproperlyConfigured, TypeError): + postgresql_base = None if postgresql_base: postgresql_base.CursorDebugWrapper = PostgreSQLPrintCursorDebugWrapper @@ -113,4 +138,6 @@ class PostgreSQLPrintCursorDebugWrapper(PrintQueryWrapperMixin, _PostgreSQLCurso if connections: for connection_name in connections: - connections[connection_name].force_debug_cursor = _force_debug_cursor[connection_name] + connections[connection_name].force_debug_cursor = _force_debug_cursor[ + connection_name + ] diff --git a/django_extensions/management/email_notifications.py b/django_extensions/management/email_notifications.py index 2c360cba7..9f070b0a4 100644 --- a/django_extensions/management/email_notifications.py +++ b/django_extensions/management/email_notifications.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import sys import traceback @@ -48,20 +47,24 @@ class EmailNotificationCommand(BaseCommand): """ def add_arguments(self, parser): - parser.add_argument('--email-notifications', - action='store_true', - default=False, - dest='email_notifications', - help='Send email notifications for command.') - parser.add_argument('--email-exception', - action='store_true', - default=False, - dest='email_exception', - help='Send email for command exceptions.') + parser.add_argument( + "--email-notifications", + action="store_true", + default=False, + dest="email_notifications", + help="Send email notifications for command.", + ) + parser.add_argument( + "--email-exception", + action="store_true", + default=False, + dest="email_exception", + help="Send email for command exceptions.", + ) def run_from_argv(self, argv): """Overriden in order to access the command line arguments.""" - self.argv_string = ' '.join(argv) + self.argv_string = " ".join(argv) super().run_from_argv(argv) def execute(self, *args, **options): @@ -76,11 +79,13 @@ def execute(self, *args, **options): try: super().execute(*args, **options) except Exception: - if options['email_exception'] or getattr(self, 'email_exception', False): + if options["email_exception"] or getattr(self, "email_exception", False): self.send_email_notification(include_traceback=True) raise - def send_email_notification(self, notification_id=None, include_traceback=False, verbosity=1): + def send_email_notification( + self, notification_id=None, include_traceback=False, verbosity=1 + ): """ Send email notifications. @@ -98,36 +103,35 @@ def send_email_notification(self, notification_id=None, include_traceback=False, email_settings = {} # Exit if no traceback found and not in 'notify always' mode - if not include_traceback and not email_settings.get('notification_level', 0): + if not include_traceback and not email_settings.get("notification_level", 0): print(self.style.ERROR("Exiting, not in 'notify always' mode.")) return # Set email fields. - subject = email_settings.get('subject', "Django extensions email notification.") + subject = email_settings.get("subject", "Django extensions email notification.") - command_name = self.__module__.split('.')[-1] + command_name = self.__module__.split(".")[-1] body = email_settings.get( - 'body', - "Reporting execution of command: '%s'" % command_name + "body", "Reporting execution of command: '%s'" % command_name ) # Include traceback - if include_traceback and not email_settings.get('no_traceback', False): + if include_traceback and not email_settings.get("no_traceback", False): try: exc_type, exc_value, exc_traceback = sys.exc_info() - trb = ''.join(traceback.format_tb(exc_traceback)) + trb = "".join(traceback.format_tb(exc_traceback)) body += "\n\nTraceback:\n\n%s\n" % trb finally: del exc_traceback # Set from address - from_email = email_settings.get('from_email', settings.DEFAULT_FROM_EMAIL) + from_email = email_settings.get("from_email", settings.DEFAULT_FROM_EMAIL) # Calculate recipients - recipients = list(email_settings.get('recipients', [])) + recipients = list(email_settings.get("recipients", [])) - if not email_settings.get('no_admins', False): + if not email_settings.get("no_admins", False): recipients.extend(settings.ADMINS) if not recipients: @@ -136,5 +140,10 @@ def send_email_notification(self, notification_id=None, include_traceback=False, return # Send email... - send_mail(subject, body, from_email, recipients, - fail_silently=email_settings.get('fail_silently', True)) + send_mail( + subject, + body, + from_email, + recipients, + fail_silently=email_settings.get("fail_silently", True), + ) diff --git a/django_extensions/management/jobs.py b/django_extensions/management/jobs.py index 1c6cdb48a..59668f052 100644 --- a/django_extensions/management/jobs.py +++ b/django_extensions/management/jobs.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- import os import sys -from imp import find_module +import importlib from typing import Optional # NOQA from django.apps import apps @@ -58,7 +57,7 @@ def my_import(name): except ImportError as err: raise JobError("Failed to import %s with error %s" % (name, err)) - mods = name.split('.') + mods = name.split(".") if len(mods) > 1: for mod in mods[1:]: imp = getattr(imp, mod) @@ -67,22 +66,31 @@ def my_import(name): def find_jobs(jobs_dir): try: - return [f[:-3] for f in os.listdir(jobs_dir) if not f.startswith('_') and f.endswith(".py")] + return sorted( + [ + f[:-3] + for f in os.listdir(jobs_dir) + if not f.startswith("_") and f.endswith(".py") + ] + ) except OSError: return [] -def find_job_module(app_name, when=None): - parts = app_name.split('.') - parts.append('jobs') +def find_job_module(app_name: str, when: Optional[str] = None) -> str: + """Find the directory path to a job module.""" + parts = app_name.split(".") + parts.append("jobs") if when: parts.append(when) - parts.reverse() - path = None - while parts: - part = parts.pop() - f, path, descr = find_module(part, path and [path] or None) - return path + module_name = ".".join(parts) + module = importlib.import_module(module_name) + + if not hasattr(module, "__path__"): + # module here is a non-package module, eg jobs.py + raise ImportError + + return module.__path__[0] def import_job(app_name, name, when=None): @@ -92,7 +100,9 @@ def import_job(app_name, name, when=None): try: job = job_mod.Job except AttributeError: - raise JobError("Job module %s does not contain class instance named 'Job'" % jobmodule) + raise JobError( + "Job module %s does not contain class instance named 'Job'" % jobmodule + ) if when and not (job.when == when or job.when is None): raise JobError("Job %s is not a %s job." % (jobmodule, when)) return job @@ -114,7 +124,16 @@ def get_jobs(when=None, only_scheduled=False): _jobs = {} for app_name in [app.name for app in apps.get_app_configs()]: - scandirs = (None, 'minutely', 'quarter_hourly', 'hourly', 'daily', 'weekly', 'monthly', 'yearly') + scandirs = ( + None, + "minutely", + "quarter_hourly", + "hourly", + "daily", + "weekly", + "monthly", + "yearly", + ) if when: scandirs = None, when for subdir in scandirs: @@ -149,7 +168,13 @@ def get_job(app_name, job_name): raise KeyError("Job not found: %s" % job_name) -def print_jobs(when=None, only_scheduled=False, show_when=True, show_appname=False, show_header=True): +def print_jobs( + when=None, + only_scheduled=False, + show_when=True, + show_appname=False, + show_header=True, +): jobmap = get_jobs(when, only_scheduled=only_scheduled) print("Job List: %i jobs" % len(jobmap)) jlist = sorted(jobmap.keys()) diff --git a/django_extensions/management/modelviz.py b/django_extensions/management/modelviz.py index 15552d07c..b46a5571d 100644 --- a/django_extensions/management/modelviz.py +++ b/django_extensions/management/modelviz.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ modelviz.py - DOT file generator for Django Models @@ -13,8 +12,16 @@ import re from django.apps import apps +from django.db.models import deletion from django.db.models.fields.related import ( - ForeignKey, ManyToManyField, OneToOneField, RelatedField, + ForeignKey, + ManyToManyField, + OneToOneField, + RelatedField, +) +from django.db.models.fields.reverse_related import ( + OneToOneRel, + ManyToOneRel, ) from django.contrib.contenttypes.fields import GenericRelation from django.template import Context, Template, loader @@ -25,11 +32,10 @@ __version__ = "1.1" __license__ = "Python" -__author__ = "Bas van Oostveen ", +__author__ = ("Bas van Oostveen ",) __contributors__ = [ - "Antonio Cavedoni " - "Stefano J. Attardi ", - "limodou ", + "Antonio Cavedoni Stefano J. Attardi ", + "limodou", "Carlo C8E Miron", "Andre Campos ", "Justin Findlay ", @@ -41,87 +47,100 @@ "Mikkel Munch Mortensen ", "Andrzej Bistram ", "Daniel Lipsitt ", + "Florian Anceau ", ] +ON_DELETE_COLORS = { + deletion.CASCADE: "red", + deletion.PROTECT: "blue", + deletion.SET_NULL: "orange", + deletion.SET_DEFAULT: "green", + deletion.SET: "yellow", + deletion.DO_NOTHING: "grey", + deletion.RESTRICT: "purple", +} + + def parse_file_or_list(arg): if not arg: return [] if isinstance(arg, (list, tuple, set)): return arg - if ',' not in arg and os.path.isfile(arg): + if "," not in arg and os.path.isfile(arg): return [e.strip() for e in open(arg).readlines()] - return [e.strip() for e in arg.split(',')] + return [e.strip() for e in arg.split(",")] class ModelGraph: def __init__(self, app_labels, **kwargs): self.graphs = [] - self.cli_options = kwargs.get('cli_options', None) - self.disable_fields = kwargs.get('disable_fields', False) - self.disable_abstract_fields = kwargs.get('disable_abstract_fields', False) - self.include_models = parse_file_or_list( - kwargs.get('include_models', "") - ) - self.all_applications = kwargs.get('all_applications', False) - self.use_subgraph = kwargs.get('group_models', False) - self.verbose_names = kwargs.get('verbose_names', False) - self.inheritance = kwargs.get('inheritance', True) + self.cli_options = kwargs.get("cli_options", None) + self.disable_fields = kwargs.get("disable_fields", False) + self.disable_abstract_fields = kwargs.get("disable_abstract_fields", False) + self.include_models = parse_file_or_list(kwargs.get("include_models", "")) + self.all_applications = kwargs.get("all_applications", False) + self.use_subgraph = kwargs.get("group_models", False) + self.verbose_names = kwargs.get("verbose_names", False) + self.inheritance = kwargs.get("inheritance", True) self.relations_as_fields = kwargs.get("relations_as_fields", True) + self.relation_fields_only = kwargs.get("relation_fields_only", False) self.sort_fields = kwargs.get("sort_fields", True) - self.language = kwargs.get('language', None) + self.language = kwargs.get("language", None) if self.language is not None: activate_language(self.language) - self.exclude_columns = parse_file_or_list( - kwargs.get('exclude_columns', "") - ) - self.exclude_models = parse_file_or_list( - kwargs.get('exclude_models', "") - ) - self.hide_edge_labels = kwargs.get('hide_edge_labels', False) + self.exclude_columns = parse_file_or_list(kwargs.get("exclude_columns", "")) + self.exclude_models = parse_file_or_list(kwargs.get("exclude_models", "")) + self.hide_edge_labels = kwargs.get("hide_edge_labels", False) self.arrow_shape = kwargs.get("arrow_shape") + self.color_code_deletions = kwargs.get("color_code_deletions", False) if self.all_applications: self.app_labels = [app.label for app in apps.get_app_configs()] else: self.app_labels = app_labels self.rankdir = kwargs.get("rankdir") + self.display_field_choices = kwargs.get("display_field_choices", False) + self.ordering = kwargs.get("ordering") def generate_graph_data(self): self.process_apps() nodes = [] for graph in self.graphs: - nodes.extend([e['name'] for e in graph['models']]) + nodes.extend([e["name"] for e in graph["models"]]) for graph in self.graphs: - for model in graph['models']: - for relation in model['relations']: + for model in graph["models"]: + for relation in model["relations"]: if relation is not None: - if relation['target'] in nodes: - relation['needs_node'] = False + if relation["target"] in nodes: + relation["needs_node"] = False def get_graph_data(self, as_json=False): now = datetime.datetime.now() graph_data = { - 'created_at': now.strftime("%Y-%m-%d %H:%M"), - 'cli_options': self.cli_options, - 'disable_fields': self.disable_fields, - 'disable_abstract_fields': self.disable_abstract_fields, - 'use_subgraph': self.use_subgraph, - 'rankdir': self.rankdir, + "created_at": now.strftime("%Y-%m-%d %H:%M"), + "cli_options": self.cli_options, + "disable_fields": self.disable_fields, + "disable_abstract_fields": self.disable_abstract_fields, + "display_field_choices": self.display_field_choices, + "use_subgraph": self.use_subgraph, + "rankdir": self.rankdir, + "ordering": self.ordering, } if as_json: - # We need to remove the model and field class because it is not JSON serializable + # We need to remove the model and field class + # because it is not JSON serializable graphs = [context.flatten() for context in self.graphs] for context in graphs: - for model_data in context['models']: - model_data.pop('model') - for field_data in model_data['fields']: - field_data.pop('field') - graph_data['graphs'] = graphs + for model_data in context["models"]: + model_data.pop("model") + for field_data in model_data["fields"]: + field_data.pop("field") + graph_data["graphs"] = graphs else: - graph_data['graphs'] = self.graphs + graph_data["graphs"] = self.graphs return graph_data @@ -136,23 +155,26 @@ def add_attributes(self, field, abstract_fields): t = type(field).__name__ if isinstance(field, (OneToOneField, ForeignKey)): t += " ({0})".format(field.remote_field.field_name) + if self.display_field_choices and field.choices is not None: + choices = {c for c, _ in field.choices} + t = str(choices) # TODO: ManyToManyField, GenericRelation return { - 'field': field, - 'name': field.name, - 'label': label, - 'type': t, - 'blank': field.blank, - 'abstract': any( + "field": field, + "name": field.name, + "label": label, + "type": t, + "blank": field.blank, + "abstract": any( field.creation_counter == abstract_field.creation_counter for abstract_field in abstract_fields ), - 'relation': isinstance(field, RelatedField), - 'primary_key': field.primary_key, + "relation": isinstance(field, RelatedField), + "primary_key": field.primary_key, } - def add_relation(self, field, model, extras=""): + def add_relation(self, field, model, extras="", color=None): if self.verbose_names and field.verbose_name: label = force_str(field.verbose_name) if label.islower(): @@ -161,21 +183,21 @@ def add_relation(self, field, model, extras=""): label = field.name # show related field name - if hasattr(field, 'related_query_name'): + if hasattr(field, "related_query_name"): related_query_name = field.related_query_name() if self.verbose_names and related_query_name.islower(): - related_query_name = related_query_name.replace('_', ' ').capitalize() - label = u'{} ({})'.format(label, force_str(related_query_name)) + related_query_name = related_query_name.replace("_", " ").capitalize() + label = "{} ({})".format(label, force_str(related_query_name)) if self.hide_edge_labels: - label = '' + label = "" # handle self-relationships and lazy-relationships if isinstance(field.remote_field.model, str): - if field.remote_field.model == 'self': + if field.remote_field.model == "self": target_model = field.model else: - if '.' in field.remote_field.model: - app_label, model_name = field.remote_field.model.split('.', 1) + if "." in field.remote_field.model: + app_label, model_name = field.remote_field.model.split(".", 1) else: app_label = field.model._meta.app_label model_name = field.remote_field.model @@ -183,65 +205,76 @@ def add_relation(self, field, model, extras=""): else: target_model = field.remote_field.model + if color: + extras = "[{}, color={}]".format(extras[1:-1], color) + _rel = self.get_relation_context(target_model, field, label, extras) - if _rel not in model['relations'] and self.use_model(_rel['target']): + if _rel not in model["relations"] and self.use_model(_rel["target"]): return _rel def get_abstract_models(self, appmodels): abstract_models = [] for appmodel in appmodels: abstract_models += [ - abstract_model for abstract_model in appmodel.__bases__ - if hasattr(abstract_model, '_meta') and abstract_model._meta.abstract + abstract_model + for abstract_model in appmodel.__bases__ + if hasattr(abstract_model, "_meta") and abstract_model._meta.abstract ] abstract_models = list(set(abstract_models)) # remove duplicates return abstract_models def get_app_context(self, app): - return Context({ - 'name': '"%s"' % app.name, - 'app_name': "%s" % app.name, - 'cluster_app_name': "cluster_%s" % app.name.replace(".", "_"), - 'models': [] - }) + return Context( + { + "name": '"%s"' % app.name, + "app_name": "%s" % app.name, + "cluster_app_name": "cluster_%s" % app.name.replace(".", "_"), + "models": [], + } + ) def get_appmodel_attributes(self, appmodel): if self.relations_as_fields: attributes = [field for field in appmodel._meta.local_fields] else: - # Find all the 'real' attributes. Relations are depicted as graph edges instead of attributes - attributes = [field for field in appmodel._meta.local_fields if not - isinstance(field, RelatedField)] + # Find all the 'real' attributes. Relations are depicted as graph edges + # instead of attributes + attributes = [ + field + for field in appmodel._meta.local_fields + if not isinstance(field, RelatedField) + ] return attributes def get_appmodel_abstracts(self, appmodel): return [ - abstract_model.__name__ for abstract_model in appmodel.__bases__ - if hasattr(abstract_model, '_meta') and abstract_model._meta.abstract + abstract_model.__name__ + for abstract_model in appmodel.__bases__ + if hasattr(abstract_model, "_meta") and abstract_model._meta.abstract ] def get_appmodel_context(self, appmodel, appmodel_abstracts): context = { - 'model': appmodel, - 'app_name': appmodel.__module__.replace(".", "_"), - 'name': appmodel.__name__, - 'abstracts': appmodel_abstracts, - 'fields': [], - 'relations': [] + "model": appmodel, + "app_name": appmodel.__module__.replace(".", "_"), + "name": appmodel.__name__, + "abstracts": appmodel_abstracts, + "fields": [], + "relations": [], } if self.verbose_names and appmodel._meta.verbose_name: - context['label'] = force_str(appmodel._meta.verbose_name) + context["label"] = force_str(appmodel._meta.verbose_name) else: - context['label'] = context['name'] + context["label"] = context["name"] return context def get_bases_abstract_fields(self, c): _abstract_fields = [] for e in c.__bases__: - if hasattr(e, '_meta') and e._meta.abstract: + if hasattr(e, "_meta") and e._meta.abstract: _abstract_fields.extend(e._meta.fields) _abstract_fields.extend(self.get_bases_abstract_fields(e)) return _abstract_fields @@ -254,15 +287,15 @@ def get_inheritance_context(self, appmodel, parent): label = "proxy" label += r"\ninheritance" if self.hide_edge_labels: - label = '' + label = "" return { - 'target_app': parent.__module__.replace(".", "_"), - 'target': parent.__name__, - 'type': "inheritance", - 'name': "inheritance", - 'label': label, - 'arrows': '[arrowhead=empty, arrowtail=none, dir=both]', - 'needs_node': True, + "target_app": parent.__module__.replace(".", "_"), + "target": parent.__name__, + "type": "inheritance", + "name": "inheritance", + "label": label, + "arrows": "[arrowhead=empty, arrowtail=none, dir=both]", + "needs_node": True, } def get_models(self, app): @@ -271,20 +304,20 @@ def get_models(self, app): def get_relation_context(self, target_model, field, label, extras): return { - 'target_app': target_model.__module__.replace('.', '_'), - 'target': target_model.__name__, - 'type': type(field).__name__, - 'name': field.name, - 'label': label, - 'arrows': extras, - 'needs_node': True + "target_app": target_model.__module__.replace(".", "_"), + "target": target_model.__name__, + "type": type(field).__name__, + "name": field.name, + "label": label, + "arrows": extras, + "needs_node": True, } def process_attributes(self, field, model, pk, abstract_fields): newmodel = model.copy() if self.skip_field(field) or pk and field == pk: return newmodel - newmodel['fields'].append(self.add_attributes(field, abstract_fields)) + newmodel["fields"].append(self.add_attributes(field, abstract_fields)) return newmodel def process_apps(self): @@ -305,10 +338,11 @@ def process_apps(self): model = self.get_appmodel_context(appmodel, appmodel_abstracts) attributes = self.get_appmodel_attributes(appmodel) - # find primary key and print it first, ignoring implicit id if other pk exists + # find primary key and print it first + # ignoring implicit id if other pk exists pk = appmodel._meta.pk if pk and not appmodel._meta.abstract and pk in attributes: - model['fields'].append(self.add_attributes(pk, abstract_fields)) + model["fields"].append(self.add_attributes(pk, abstract_fields)) for field in attributes: model = self.process_attributes(field, model, pk, abstract_fields) @@ -327,32 +361,42 @@ def process_apps(self): for parent in appmodel.__bases__: model = self.process_parent(parent, appmodel, model) - app_graph['models'].append(model) - if app_graph['models']: + app_graph["models"].append(model) + if app_graph["models"]: self.graphs.append(app_graph) def process_local_fields(self, field, model, abstract_fields): newmodel = model.copy() - if field.attname.endswith('_ptr_id') or field in abstract_fields or self.skip_field(field): + if ( + field.attname.endswith("_ptr_id") + or field in abstract_fields + or self.skip_field(field) + ): # excluding field redundant with inheritance relation - # excluding fields inherited from abstract classes. they too show as local_fields + # excluding fields inherited from abstract classes. + # they too show as local_fields return newmodel + + color = None + if self.color_code_deletions and isinstance(field, (OneToOneField, ForeignKey)): + field_on_delete = getattr(field.remote_field, "on_delete", None) + color = ON_DELETE_COLORS.get(field_on_delete) + if isinstance(field, OneToOneField): relation = self.add_relation( - field, newmodel, '[arrowhead=none, arrowtail=none, dir=both]' + field, newmodel, "[arrowhead=none, arrowtail=none, dir=both]", color ) elif isinstance(field, ForeignKey): relation = self.add_relation( field, newmodel, - '[arrowhead=none, arrowtail={}, dir=both]'.format( - self.arrow_shape - ), + "[arrowhead=none, arrowtail={}, dir=both]".format(self.arrow_shape), + color, ) else: relation = None if relation is not None: - newmodel['relations'].append(relation) + newmodel["relations"].append(relation) return newmodel def process_local_many_to_many(self, field, model): @@ -361,32 +405,49 @@ def process_local_many_to_many(self, field, model): return newmodel relation = None if isinstance(field, ManyToManyField): - if hasattr(field.remote_field.through, '_meta') and field.remote_field.through._meta.auto_created: + if ( + hasattr(field.remote_field.through, "_meta") + and field.remote_field.through._meta.auto_created + ): relation = self.add_relation( field, newmodel, - '[arrowhead={} arrowtail={}, dir=both]'.format( + "[arrowhead={} arrowtail={}, dir=both]".format( self.arrow_shape, self.arrow_shape ), ) elif isinstance(field, GenericRelation): - relation = self.add_relation(field, newmodel, mark_safe('[style="dotted", arrowhead=normal, arrowtail=normal, dir=both]')) + relation = self.add_relation( + field, + newmodel, + mark_safe( + '[style="dotted", arrowhead=normal, arrowtail=normal, dir=both]' + ), + ) if relation is not None: - newmodel['relations'].append(relation) + newmodel["relations"].append(relation) return newmodel def process_parent(self, parent, appmodel, model): newmodel = model.copy() if hasattr(parent, "_meta"): # parent is a model _rel = self.get_inheritance_context(appmodel, parent) - # TODO: seems as if abstract models aren't part of models.getModels, which is why they are printed by this without any attributes. - if _rel not in newmodel['relations'] and self.use_model(_rel['target']): - newmodel['relations'].append(_rel) + # TODO: seems as if abstract models aren't part of models.getModels, + # which is why they are printed by this without any attributes. + if _rel not in newmodel["relations"] and self.use_model(_rel["target"]): + newmodel["relations"].append(_rel) return newmodel def sort_model_fields(self, model): newmodel = model.copy() - newmodel['fields'] = sorted(newmodel['fields'], key=lambda field: (not field['primary_key'], not field['relation'], field['label'])) + newmodel["fields"] = sorted( + newmodel["fields"], + key=lambda field: ( + not field["primary_key"], + not field["relation"], + field["label"], + ), + ) return newmodel def use_model(self, model_name): @@ -397,13 +458,13 @@ def use_model(self, model_name): # Check against include list. if self.include_models: for model_pattern in self.include_models: - model_pattern = '^%s$' % model_pattern.replace('*', '.*') + model_pattern = "^%s$" % model_pattern.replace("*", ".*") if re.search(model_pattern, model_name): return True # Check against exclude list. if self.exclude_models: for model_pattern in self.exclude_models: - model_pattern = '^%s$' % model_pattern.replace('*', '.*') + model_pattern = "^%s$" % model_pattern.replace("*", ".*") if re.search(model_pattern, model_name): return False # Return `True` if `include_models` is falsey, otherwise return `False`. @@ -416,17 +477,34 @@ def skip_field(self, field): return True if field.name in self.exclude_columns: return True + if self.relation_fields_only: + if not isinstance( + field, + ( + ForeignKey, + ManyToManyField, + OneToOneField, + RelatedField, + OneToOneRel, + ManyToOneRel, + ), + ): + return True return False -def generate_dot(graph_data, template='django_extensions/graph_models/digraph.dot'): +def generate_dot(graph_data, template="django_extensions/graph_models/digraph.dot"): if isinstance(template, str): template = loader.get_template(template) - if not isinstance(template, Template) and not (hasattr(template, 'template') and isinstance(template.template, Template)): - raise Exception("Default Django template loader isn't used. " - "This can lead to the incorrect template rendering. " - "Please, check the settings.") + if not isinstance(template, Template) and not ( + hasattr(template, "template") and isinstance(template.template, Template) + ): + raise Exception( + "Default Django template loader isn't used. " + "This can lead to the incorrect template rendering. " + "Please, check the settings." + ) c = Context(graph_data).flatten() dot = template.render(c) @@ -441,5 +519,7 @@ def generate_graph_data(*args, **kwargs): def use_model(model, include_models, exclude_models): - generator = ModelGraph([], include_models=include_models, exclude_models=exclude_models) + generator = ModelGraph( + [], include_models=include_models, exclude_models=exclude_models + ) return generator.use_model(model) diff --git a/django_extensions/management/mysql.py b/django_extensions/management/mysql.py index 4b3f7fdf6..0adec2900 100644 --- a/django_extensions/management/mysql.py +++ b/django_extensions/management/mysql.py @@ -1,37 +1,39 @@ -# -*- coding: utf-8 -*- import configparser def parse_mysql_cnf(dbinfo): """ Attempt to parse mysql database config file for connection settings. - Ideally we would hook into django's code to do this, but read_default_file is handled by the mysql C libs - so we have to emulate the behaviour + Ideally we would hook into django's code to do this, but read_default_file is + handled by the mysql C libs so we have to emulate the behaviour Settings that are missing will return '' returns (user, password, database_name, database_host, database_port) """ - read_default_file = dbinfo.get('OPTIONS', {}).get('read_default_file') + read_default_file = dbinfo.get("OPTIONS", {}).get("read_default_file") if read_default_file: - config = configparser.RawConfigParser({ - 'user': '', - 'password': '', - 'database': '', - 'host': '', - 'port': '', - 'socket': '', - }) + config = configparser.RawConfigParser( + { + "user": "", + "password": "", + "database": "", + "host": "", + "port": "", + "socket": "", + } + ) import os + config.read(os.path.expanduser(read_default_file)) try: - user = config.get('client', 'user') - password = config.get('client', 'password') - database_name = config.get('client', 'database') - database_host = config.get('client', 'host') - database_port = config.get('client', 'port') - socket = config.get('client', 'socket') - - if database_host == 'localhost' and socket: + user = config.get("client", "user") + password = config.get("client", "password") + database_name = config.get("client", "database") + database_host = config.get("client", "host") + database_port = config.get("client", "port") + socket = config.get("client", "socket") + + if database_host == "localhost" and socket: # mysql actually uses a socket if host is localhost database_host = socket @@ -40,4 +42,4 @@ def parse_mysql_cnf(dbinfo): except configparser.NoSectionError: pass - return '', '', '', '', '' + return "", "", "", "", "" diff --git a/django_extensions/management/notebook_extension.py b/django_extensions/management/notebook_extension.py index bd072737c..477a6b986 100644 --- a/django_extensions/management/notebook_extension.py +++ b/django_extensions/management/notebook_extension.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- def load_ipython_extension(ipython): from django.core.management.color import no_style from django_extensions.management.shells import import_objects imported_objects = import_objects( - options={'dont_load': []}, + options={"dont_load": []}, style=no_style(), ) ipython.push(imported_objects) diff --git a/django_extensions/management/shells.py b/django_extensions/management/shells.py index 3fc9c6a54..4afb9e9da 100644 --- a/django_extensions/management/shells.py +++ b/django_extensions/management/shells.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import ast import traceback import warnings @@ -19,14 +18,14 @@ SHELL_PLUS_DJANGO_IMPORTS = [ - 'from django.core.cache import cache', - 'from django.conf import settings', - 'from django.contrib.auth import get_user_model', - 'from django.db import transaction', - 'from django.db.models import Avg, Case, Count, F, Max, Min, Prefetch, Q, Sum, When', - 'from django.utils import timezone', - 'from django.urls import reverse', - 'from django.db.models import Exists, OuterRef, Subquery', + "from django.core.cache import cache", + "from django.conf import settings", + "from django.contrib.auth import get_user_model", + "from django.db import transaction", + "from django.db.models import Avg, Case, Count, F, Max, Min, Prefetch, Q, Sum, When", # noqa: E501 + "from django.utils import timezone", + "from django.urls import reverse", + "from django.db.models import Exists, OuterRef, Subquery", ] @@ -51,7 +50,7 @@ def get_app_name(mod_name): >>> get_app_name('some.testapp.foo') 'testapp' """ - rparts = list(reversed(mod_name.split('.'))) + rparts = list(reversed(mod_name.split("."))) try: try: return rparts[rparts.index(MODELS_MODULE_NAME) + 1] @@ -74,23 +73,31 @@ def import_items(import_directives, style, quiet_load=False): * a simple 'module.submodule' which indicates 'import module.submodule'. Returns a dict mapping the names to the imported items - """ + """ # noqa: E501 imported_objects = {} for directive in import_directives: if isinstance(directive, str): directive = directive.strip() try: - if isinstance(directive, str) and directive.startswith(("from ", "import ")): + if isinstance(directive, str) and directive.startswith( + ("from ", "import ") + ): try: node = ast.parse(directive) except Exception as exc: if not quiet_load: print(style.ERROR("Error parsing: %r %s" % (directive, exc))) continue - if not all(isinstance(body, (ast.Import, ast.ImportFrom)) for body in node.body): + if not all( + isinstance(body, (ast.Import, ast.ImportFrom)) for body in node.body + ): if not quiet_load: - print(style.ERROR("Only specify import statements: %r" % directive)) + print( + style.ERROR( + "Only specify import statements: %r" % directive + ) + ) continue if not quiet_load: @@ -100,38 +107,66 @@ def import_items(import_directives, style, quiet_load=False): if isinstance(body, ast.Import): for name in body.names: asname = name.asname or name.name - imported_objects[asname] = importlib.import_module(name.name) + imported_objects[asname] = importlib.import_module( + name.name + ) if isinstance(body, ast.ImportFrom): - imported_object = importlib.__import__(body.module, {}, {}, [name.name for name in body.names]) + imported_object = importlib.__import__( + body.module, {}, {}, [name.name for name in body.names] + ) for name in body.names: asname = name.asname or name.name try: if name.name == "*": for k in dir(imported_object): - imported_objects[k] = getattr(imported_object, k) + imported_objects[k] = getattr( + imported_object, k + ) else: - imported_objects[asname] = getattr(imported_object, name.name) + imported_objects[asname] = getattr( + imported_object, name.name + ) except AttributeError as exc: - print(dir(imported_object)) + print( + ( + f"Couldn't find {name.name} in {body.module}. " + f"Only found: {dir(imported_object)}" + ) + ) # raise raise ImportError(exc) else: - warnings.warn("Old style import definitions are deprecated. You should use the new style which is similar to normal Python imports. ", RemovedInNextVersionWarning, stacklevel=2) + warnings.warn( + "Old style import definitions are deprecated. You should use the new style which is similar to normal Python imports. ", # noqa: E501 + RemovedInNextVersionWarning, + stacklevel=2, + ) if isinstance(directive, str): imported_object = __import__(directive) - imported_objects[directive.split('.')[0]] = imported_object + imported_objects[directive.split(".")[0]] = imported_object if not quiet_load: print(style.SQL_COLTYPE("import %s" % directive)) continue elif isinstance(directive, (list, tuple)) and len(directive) == 2: if not isinstance(directive[0], str): if not quiet_load: - print(style.ERROR("Unable to import %r: module name must be of type string" % directive[0])) + print( + style.ERROR( + ( + "Unable to import %r: module name must be of " + "type string" + ) + % directive[0] + ) + ) continue - if isinstance(directive[1], (list, tuple)) and all(isinstance(e, str) for e in directive[1]): - # Try the ('module.submodule', ('classname1', 'classname2')) form + if isinstance(directive[1], (list, tuple)) and all( + isinstance(e, str) for e in directive[1] + ): + # Try the form of: + # ('module.submodule', ('classname1', 'classname2')) imported_object = __import__(directive[0], {}, {}, directive[1]) imported_names = [] for name in directive[1]: @@ -139,31 +174,63 @@ def import_items(import_directives, style, quiet_load=False): imported_objects[name] = getattr(imported_object, name) except AttributeError: if not quiet_load: - print(style.ERROR("Unable to import %r from %r: %r does not exist" % (name, directive[0], name))) + print( + style.ERROR( + "Unable to import %r from %r: %r does not " + "exist" % (name, directive[0], name) + ) + ) else: imported_names.append(name) if not quiet_load: - print(style.SQL_COLTYPE("from %s import %s" % (directive[0], ', '.join(imported_names)))) + print( + style.SQL_COLTYPE( + "from %s import %s" + % (directive[0], ", ".join(imported_names)) + ) + ) elif isinstance(directive[1], str): - # If it is a tuple, but the second item isn't a list, so we have something like ('module.submodule', 'classname1') + # If it is a tuple, but the second item isn't a list, so we have + # something like ('module.submodule', 'classname1') # Check for the special '*' to import all - if directive[1] == '*': - imported_object = __import__(directive[0], {}, {}, directive[1]) + if directive[1] == "*": + imported_object = __import__( + directive[0], {}, {}, directive[1] + ) for k in dir(imported_object): imported_objects[k] = getattr(imported_object, k) if not quiet_load: - print(style.SQL_COLTYPE("from %s import *" % directive[0])) + print( + style.SQL_COLTYPE("from %s import *" % directive[0]) + ) else: - imported_object = getattr(__import__(directive[0], {}, {}, [directive[1]]), directive[1]) + imported_object = getattr( + __import__(directive[0], {}, {}, [directive[1]]), + directive[1], + ) imported_objects[directive[1]] = imported_object if not quiet_load: - print(style.SQL_COLTYPE("from %s import %s" % (directive[0], directive[1]))) + print( + style.SQL_COLTYPE( + "from %s import %s" + % (directive[0], directive[1]) + ) + ) else: if not quiet_load: - print(style.ERROR("Unable to import %r from %r: names must be of type string" % (directive[1], directive[0]))) + print( + style.ERROR( + "Unable to import %r from %r: names must be strings" + % (directive[1], directive[0]) + ) + ) else: if not quiet_load: - print(style.ERROR("Unable to import %r: names must be of type string" % directive)) + print( + style.ERROR( + "Unable to import %r: names must be strings" % directive + ) + ) except ImportError: if not quiet_load: print(style.ERROR("Unable to import %r" % directive)) @@ -180,14 +247,14 @@ def import_objects(options, style): from django.conf import settings - dont_load_cli = options.get('dont_load', []) - dont_load_conf = getattr(settings, 'SHELL_PLUS_DONT_LOAD', []) + dont_load_cli = options.get("dont_load", []) + dont_load_conf = getattr(settings, "SHELL_PLUS_DONT_LOAD", []) dont_load = dont_load_cli + dont_load_conf - dont_load_any_models = '*' in dont_load - quiet_load = options.get('quiet_load') - model_aliases = getattr(settings, 'SHELL_PLUS_MODEL_ALIASES', {}) - app_prefixes = getattr(settings, 'SHELL_PLUS_APP_PREFIXES', {}) - SHELL_PLUS_PRE_IMPORTS = getattr(settings, 'SHELL_PLUS_PRE_IMPORTS', {}) + dont_load_any_models = "*" in dont_load + quiet_load = options.get("quiet_load") + model_aliases = getattr(settings, "SHELL_PLUS_MODEL_ALIASES", {}) + app_prefixes = getattr(settings, "SHELL_PLUS_APP_PREFIXES", {}) + SHELL_PLUS_PRE_IMPORTS = getattr(settings, "SHELL_PLUS_PRE_IMPORTS", {}) imported_objects = {} load_models = {} @@ -199,7 +266,7 @@ def get_dict_from_names_to_possible_models(): # type: () -> Dict[str, List[str] This dictionary is used by collision resolver. At this phase we can't import any models, because collision resolver can change results. :return: Dict[str, List[str]]. Key is name, value is list of full model's path's. - """ + """ # noqa: E501 models_to_import = {} # type: Dict[str, List[str]] for app_mod, models in sorted(load_models.items()): app_name = get_app_name(app_mod) @@ -223,19 +290,23 @@ def get_dict_from_names_to_possible_models(): # type: () -> Dict[str, List[str] return models_to_import def import_subclasses(): - base_classes_to_import = getattr(settings, 'SHELL_PLUS_SUBCLASSES_IMPORT', []) # type: List[Union[str, type]] + base_classes_to_import = getattr(settings, "SHELL_PLUS_SUBCLASSES_IMPORT", []) # type: List[Union[str, type]] if base_classes_to_import: if not quiet_load: print(style.SQL_TABLE("# Shell Plus Subclasses Imports")) - perform_automatic_imports(SubclassesFinder(base_classes_to_import).collect_subclasses()) + perform_automatic_imports( + SubclassesFinder(base_classes_to_import).collect_subclasses() + ) def import_models(): """ Perform collision resolving and imports all models. When collisions are resolved we can perform imports and print information's, because it is last phase. This function updates imported_objects dictionary. - """ - modules_to_models = CollisionResolvingRunner().run_collision_resolver(get_dict_from_names_to_possible_models()) + """ # noqa: E501 + modules_to_models = CollisionResolvingRunner().run_collision_resolver( + get_dict_from_names_to_possible_models() + ) perform_automatic_imports(modules_to_models) def perform_automatic_imports(modules_to_classes): # type: (Dict[str, List[Tuple[str, str]]]) -> () @@ -247,9 +318,11 @@ def perform_automatic_imports(modules_to_classes): # type: (Dict[str, List[Tupl """ for full_module_path, models in modules_to_classes.items(): model_labels = [] - for (model_name, alias) in sorted(models): + for model_name, alias in sorted(models): try: - imported_objects[alias] = import_string("%s.%s" % (full_module_path, model_name)) + imported_objects[alias] = import_string( + "%s.%s" % (full_module_path, model_name) + ) if model_name == alias: model_labels.append(model_name) else: @@ -257,11 +330,20 @@ def perform_automatic_imports(modules_to_classes): # type: (Dict[str, List[Tupl except ImportError as e: if options.get("traceback"): traceback.print_exc() - if not options.get('quiet_load'): - print(style.ERROR( - "Failed to import '%s' from '%s' reason: %s" % (model_name, full_module_path, str(e)))) - if not options.get('quiet_load'): - print(style.SQL_COLTYPE("from %s import %s" % (full_module_path, ", ".join(model_labels)))) + if not options.get("quiet_load"): + print( + style.ERROR( + "Failed to import '%s' from '%s' reason: %s" + % (model_name, full_module_path, str(e)) + ) + ) + if not options.get("quiet_load"): + print( + style.SQL_COLTYPE( + "from %s import %s" + % (full_module_path, ", ".join(model_labels)) + ) + ) def get_apps_and_models(): for app in apps.get_app_configs(): @@ -271,6 +353,7 @@ def get_apps_and_models(): mongoengine = False try: from mongoengine.base import _document_registry + mongoengine = True except ImportError: pass @@ -285,7 +368,7 @@ def get_apps_and_models(): if mongoengine and not dont_load_any_models: for name, mod in _document_registry.items(): - name = name.split('.')[-1] + name = name.split(".")[-1] app_name = get_app_name(mod.__module__) if app_name in dont_load or ("%s.%s" % (app_name, name)) in dont_load: continue @@ -313,19 +396,22 @@ def get_apps_and_models(): import_subclasses() if not quiet_load: - print(style.SQL_TABLE("# Shell Plus Model Imports%s") % (' SKIPPED' if dont_load_any_models else '')) + print( + style.SQL_TABLE("# Shell Plus Model Imports%s") + % (" SKIPPED" if dont_load_any_models else "") + ) import_models() # Imports often used from Django - if getattr(settings, 'SHELL_PLUS_DJANGO_IMPORTS', True): + if getattr(settings, "SHELL_PLUS_DJANGO_IMPORTS", True): if not quiet_load: print(style.SQL_TABLE("# Shell Plus Django Imports")) imports = import_items(SHELL_PLUS_DJANGO_IMPORTS, style, quiet_load=quiet_load) for k, v in imports.items(): imported_objects[k] = v - SHELL_PLUS_IMPORTS = getattr(settings, 'SHELL_PLUS_IMPORTS', {}) + SHELL_PLUS_IMPORTS = getattr(settings, "SHELL_PLUS_IMPORTS", {}) if SHELL_PLUS_IMPORTS: if not quiet_load: print(style.SQL_TABLE("# Shell Plus User Imports")) @@ -334,7 +420,7 @@ def get_apps_and_models(): imported_objects[k] = v # Perform post-imports after any other imports - SHELL_PLUS_POST_IMPORTS = getattr(settings, 'SHELL_PLUS_POST_IMPORTS', {}) + SHELL_PLUS_POST_IMPORTS = getattr(settings, "SHELL_PLUS_POST_IMPORTS", {}) if SHELL_PLUS_POST_IMPORTS: if not quiet_load: print(style.SQL_TABLE("# Shell Plus User Post Imports")) diff --git a/django_extensions/management/signals.py b/django_extensions/management/signals.py index 3a490cf36..378d30aad 100644 --- a/django_extensions/management/signals.py +++ b/django_extensions/management/signals.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.dispatch import Signal run_minutely_jobs = Signal() diff --git a/django_extensions/management/technical_response.py b/django_extensions/management/technical_response.py index 509c5ecbc..fd98cea59 100644 --- a/django_extensions/management/technical_response.py +++ b/django_extensions/management/technical_response.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import threading from django.core.handlers.wsgi import WSGIHandler @@ -23,10 +22,11 @@ def null_technical_500_response(request, exc_type, exc_value, tb, status_code=50 Runserver_plus only needs needs traceback frames relevant to WSGIHandler Middleware objects, so only store the traceback if it is for a WSGIHandler. If an exception is not raised here, Django eventually throws an error for not getting a valid response object for its debug view. - """ + """ # noqa: E501 try: - # Store the most recent tb for WSGI requests. The class can be found in the second frame of the tb - if isinstance(tb.tb_next.tb_frame.f_locals.get('self'), WSGIHandler): + # Store the most recent tb for WSGI requests. + # The class can be found in the second frame of the tb + if isinstance(tb.tb_next.tb_frame.f_locals.get("self"), WSGIHandler): tld.wsgi_tb = tb elif tld.wsgi_tb: tb = tld.wsgi_tb diff --git a/django_extensions/management/utils.py b/django_extensions/management/utils.py index 3a5ed51d9..f12bc82f0 100644 --- a/django_extensions/management/utils.py +++ b/django_extensions/management/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import logging import os import sys @@ -12,7 +11,8 @@ def _make_writeable(filename): read-only. """ import stat - if sys.platform.startswith('java'): + + if sys.platform.startswith("java"): # On Jython there is no os.access() return if not os.access(filename, os.W_OK): @@ -37,7 +37,9 @@ def setup_logger(logger, stream, filename=None, fmt=None): if filename: outfile = logging.FileHandler(filename) outfile.setLevel(logging.INFO) - outfile.setFormatter(logging.Formatter("%(asctime)s " + (fmt if fmt else '%(message)s'))) + outfile.setFormatter( + logging.Formatter("%(asctime)s " + (fmt if fmt else "%(message)s")) + ) logger.addHandler(outfile) @@ -45,7 +47,6 @@ class RedirectHandler(logging.Handler): """Redirect logging sent to one logger (name) to another.""" def __init__(self, name, level=logging.DEBUG): - # Contemplate feasibility of copying a destination (allow original handler) and redirecting. logging.Handler.__init__(self, level) self.name = name self.logger = logging.getLogger(name) @@ -55,13 +56,19 @@ def emit(self, record): def signalcommand(func): - """Python decorator for management command handle defs that sends out a pre/post signal.""" + """decorator for management command handle defs that sends out a pre/post signal.""" def inner(self, *args, **kwargs): pre_command.send(self.__class__, args=args, kwargs=kwargs) - ret = func(self, *args, **kwargs) - post_command.send(self.__class__, args=args, kwargs=kwargs, outcome=ret) + try: + ret = func(self, *args, **kwargs) + except Exception as e: + post_command.send(self.__class__, args=args, kwargs=kwargs, outcome=e) + raise + else: + post_command.send(self.__class__, args=args, kwargs=kwargs, outcome=ret) return ret + return inner @@ -69,6 +76,7 @@ def has_ipdb(): try: import ipdb # noqa import IPython # noqa + return True except ImportError: return False diff --git a/django_extensions/mongodb/fields/__init__.py b/django_extensions/mongodb/fields/__init__.py index 857a5dba0..77754a38e 100644 --- a/django_extensions/mongodb/fields/__init__.py +++ b/django_extensions/mongodb/fields/__init__.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- """ MongoDB model fields emulating Django Extensions' additional model fields -These fields are essentially identical to existing Extensions fields, but South hooks have been removed (since mongo requires no schema migration) - +These fields are essentially identical to existing Extensions fields. """ import re @@ -21,17 +19,17 @@ class SlugField(StringField): description = _("String (up to %(max_length)s)") def __init__(self, *args, **kwargs): - kwargs['max_length'] = kwargs.get('max_length', 50) + kwargs["max_length"] = kwargs.get("max_length", 50) # Set db_index=True unless it's been set manually. - if 'db_index' not in kwargs: - kwargs['db_index'] = True + if "db_index" not in kwargs: + kwargs["db_index"] = True super().__init__(*args, **kwargs) def get_internal_type(self): return "SlugField" def formfield(self, **kwargs): - defaults = {'form_class': forms.SlugField} + defaults = {"form_class": forms.SlugField} defaults.update(kwargs) return super().formfield(**defaults) @@ -56,22 +54,22 @@ class AutoSlugField(SlugField): If set to True, overwrites the slug on every save (default: False) Inspired by SmileyChris' Unique Slugify snippet: - http://www.djangosnippets.org/snippets/690/ + https://www.djangosnippets.org/snippets/690/ """ def __init__(self, *args, **kwargs): - kwargs.setdefault('blank', True) - kwargs.setdefault('editable', False) + kwargs.setdefault("blank", True) + kwargs.setdefault("editable", False) - populate_from = kwargs.pop('populate_from', None) + populate_from = kwargs.pop("populate_from", None) if populate_from is None: raise ValueError("missing 'populate_from' argument") else: self._populate_from = populate_from - self.slugify_function = kwargs.pop('slugify_function', slugify) - self.separator = kwargs.pop('separator', str('-')) - self.overwrite = kwargs.pop('overwrite', False) + self.slugify_function = kwargs.pop("slugify_function", slugify) + self.separator = kwargs.pop("separator", str("-")) + self.overwrite = kwargs.pop("overwrite", False) super().__init__(*args, **kwargs) def _slug_strip(self, value): @@ -82,9 +80,9 @@ def _slug_strip(self, value): If an alternate separator is used, it will also replace any instances of the default '-' separator with the new separator. """ - re_sep = '(?:-|%s)' % re.escape(self.separator) - value = re.sub('%s+' % re_sep, self.separator, value) - return re.sub(r'^%s+|%s+$' % (re_sep, re_sep), '', value) + re_sep = "(?:-|%s)" % re.escape(self.separator) + value = re.sub("%s+" % re_sep, self.separator, value) + return re.sub(r"^%s+|%s+$" % (re_sep, re_sep), "", value) def slugify_func(self, content): return self.slugify_function(content) @@ -92,12 +90,14 @@ def slugify_func(self, content): def create_slug(self, model_instance, add): # get fields to populate from and slug field to set if not isinstance(self._populate_from, (list, tuple)): - self._populate_from = (self._populate_from, ) + self._populate_from = (self._populate_from,) slug_field = model_instance._meta.get_field(self.attname) if add or self.overwrite: # slugify the original field content and set next step to 2 - slug_for_field = lambda lookup_value: self.slugify_func(self.get_slug_fields(model_instance, lookup_value)) + slug_for_field = lambda lookup_value: self.slugify_func( + self.get_slug_fields(model_instance, lookup_value) + ) slug = self.separator.join(map(slug_for_field, self._populate_from)) next = 2 else: @@ -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: @@ -137,12 +137,12 @@ def create_slug(self, model_instance, add): # depending on the given slug, clean-up while not slug or queryset.filter(**kwargs): slug = original_slug - end = '%s%s' % (self.separator, next) + end = "%s%s" % (self.separator, next) end_len = len(end) if slug_len and len(slug) + end_len > slug_len: - slug = slug[:slug_len - end_len] + slug = slug[: slug_len - end_len] slug = self._slug_strip(slug) - slug = '%s%s' % (slug, end) + slug = "%s%s" % (slug, end) kwargs[self.attname] = slug next += 1 return slug @@ -155,8 +155,10 @@ def get_slug_fields(self, model_instance, lookup_value): attr = getattr(attr, elem) except AttributeError: raise AttributeError( - "value {} in AutoSlugField's 'populate_from' argument {} returned an error - {} has no attribute {}".format( - elem, lookup_value, attr, elem)) + "value {} in AutoSlugField's 'populate_from' argument {} returned an error - {} has no attribute {}".format( # noqa: E501 + elem, lookup_value, attr, elem + ) + ) if callable(attr): return "%s" % attr() @@ -180,7 +182,7 @@ class CreationDateTimeField(DateTimeField): """ def __init__(self, *args, **kwargs): - kwargs.setdefault('default', datetime.datetime.now) + kwargs.setdefault("default", datetime.datetime.now) DateTimeField.__init__(self, *args, **kwargs) def get_internal_type(self): @@ -216,11 +218,21 @@ class UUIDField(StringField): By default uses UUID version 1 (generate from host ID, sequence number and current time) The field support all uuid versions which are natively supported by the uuid python module. - For more information see: http://docs.python.org/lib/module-uuid.html - """ - - def __init__(self, verbose_name=None, name=None, auto=True, version=1, node=None, clock_seq=None, namespace=None, **kwargs): - kwargs['max_length'] = 36 + For more information see: https://docs.python.org/lib/module-uuid.html + """ # noqa: E501 + + def __init__( + self, + verbose_name=None, + name=None, + auto=True, + version=1, + node=None, + clock_seq=None, + namespace=None, + **kwargs, + ): + kwargs["max_length"] = 36 self.auto = auto self.version = version if version == 1: @@ -234,7 +246,10 @@ def get_internal_type(self): def contribute_to_class(self, cls, name): if self.primary_key: - assert not cls._meta.has_auto_field, "A model can't have more than one AutoField: %s %s %s; have %s" % (self, cls, name, cls._meta.auto_field) + assert not cls._meta.has_auto_field, ( + "A model can't have more than one AutoField: %s %s %s; have %s" + % (self, cls, name, cls._meta.auto_field) + ) super().contribute_to_class(cls, name) cls._meta.has_auto_field = True cls._meta.auto_field = self diff --git a/django_extensions/mongodb/fields/json.py b/django_extensions/mongodb/fields/json.py index 042603b92..e7c10ef6b 100644 --- a/django_extensions/mongodb/fields/json.py +++ b/django_extensions/mongodb/fields/json.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ JSONField automatically serializes most Python terms to JSON data. Creates a TEXT field with a default value of "{}". See test_json.py for @@ -24,8 +23,8 @@ def default(self, obj): if isinstance(obj, Decimal): return str(obj) elif isinstance(obj, datetime.datetime): - assert settings.TIME_ZONE == 'UTC' - return obj.strftime('%Y-%m-%dT%H:%M:%SZ') + assert settings.TIME_ZONE == "UTC" + return obj.strftime("%Y-%m-%dT%H:%M:%SZ") return json.JSONEncoder.default(self, obj) @@ -57,12 +56,12 @@ class JSONField(StringField): """ def __init__(self, *args, **kwargs): - if 'default' not in kwargs: - kwargs['default'] = '{}' + if "default" not in kwargs: + kwargs["default"] = "{}" StringField.__init__(self, *args, **kwargs) def to_python(self, value): - """ Convert our string value to JSON after we load it from the DB """ + """Convert our string value to JSON after we load it from the DB""" if not value: return {} elif isinstance(value, str): @@ -73,7 +72,7 @@ def to_python(self, value): return value def get_db_prep_save(self, value): - """ Convert our JSON object to a string before we save """ + """Convert our JSON object to a string before we save""" if not value: return super().get_db_prep_save("") else: diff --git a/django_extensions/mongodb/models.py b/django_extensions/mongodb/models.py index 2c2d51d38..19c67cd4f 100644 --- a/django_extensions/mongodb/models.py +++ b/django_extensions/mongodb/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import datetime from django.utils.translation import gettext_lazy as _ @@ -6,7 +5,11 @@ from mongoengine.fields import DateTimeField, IntField, StringField from mongoengine.queryset import QuerySetManager -from django_extensions.mongodb.fields import AutoSlugField, CreationDateTimeField, ModificationDateTimeField +from django_extensions.mongodb.fields import ( + AutoSlugField, + CreationDateTimeField, + ModificationDateTimeField, +) class TimeStampedModel(Document): @@ -33,7 +36,7 @@ class TitleSlugDescriptionModel(Document): """ title = StringField(max_length=255) - slug = AutoSlugField(populate_from='title') + slug = AutoSlugField(populate_from="title") description = StringField(blank=True, null=True) class Meta: @@ -44,7 +47,8 @@ class ActivatorModelManager(QuerySetManager): """ ActivatorModelManager - Manager to return instances of ActivatorModel: SomeModel.objects.active() / .inactive() + Manager to return instances of ActivatorModel: + SomeModel.objects.active() / .inactive() """ def active(self): @@ -72,12 +76,16 @@ class ActivatorModel(Document): """ STATUS_CHOICES = ( - (0, _('Inactive')), - (1, _('Active')), + (0, _("Inactive")), + (1, _("Active")), ) status = IntField(choices=STATUS_CHOICES, default=1) - activate_date = DateTimeField(blank=True, null=True, help_text=_('keep empty for an immediate activation')) - deactivate_date = DateTimeField(blank=True, null=True, help_text=_('keep empty for indefinite activation')) + activate_date = DateTimeField( + blank=True, null=True, help_text=_("keep empty for an immediate activation") + ) + deactivate_date = DateTimeField( + blank=True, null=True, help_text=_("keep empty for indefinite activation") + ) objects = ActivatorModelManager() class Meta: diff --git a/django_extensions/py.typed b/django_extensions/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/django_extensions/settings.py b/django_extensions/settings.py index 4b71b677b..b35c84cad 100644 --- a/django_extensions/settings.py +++ b/django_extensions/settings.py @@ -1,32 +1,46 @@ -# -*- coding: utf-8 -*- import os from django.conf import settings BASE_DIR = os.path.dirname(os.path.realpath(__file__)) -REPLACEMENTS = getattr(settings, 'EXTENSIONS_REPLACEMENTS', {}) +REPLACEMENTS = getattr(settings, "EXTENSIONS_REPLACEMENTS", {}) DEFAULT_SQLITE_ENGINES = ( - 'django.db.backends.sqlite3', - 'django.db.backends.spatialite', + "django.db.backends.sqlite3", + "django.db.backends.spatialite", + "django_prometheus.db.backends.sqlite3", ) DEFAULT_MYSQL_ENGINES = ( - 'django.db.backends.mysql', - 'django.contrib.gis.db.backends.mysql', - 'mysql.connector.django', + "django.db.backends.mysql", + "django.contrib.gis.db.backends.mysql", + "django_prometheus.db.backends.mysql", + "mysql.connector.django", ) DEFAULT_POSTGRESQL_ENGINES = ( - 'django.db.backends.postgresql', - 'django.db.backends.postgresql_psycopg2', - 'django.db.backends.postgis', - 'django.contrib.gis.db.backends.postgis', - 'psqlextra.backend', - 'django_zero_downtime_migrations.backends.postgres', - 'django_zero_downtime_migrations.backends.postgis', + "django.db.backends.postgresql", + "django.db.backends.postgresql_psycopg2", + "django.db.backends.postgis", + "django.contrib.gis.db.backends.postgis", + "psqlextra.backend", + "django_zero_downtime_migrations.backends.postgres", + "django_zero_downtime_migrations.backends.postgis", + "django_prometheus.db.backends.postgresql", + "django_prometheus.db.backends.postgis", + "django_tenants.postgresql_backend", ) -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) +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 + +RUNSERVER_PLUS_EXCLUDE_PATTERNS = ["**/__pycache__/*"] diff --git a/django_extensions/static/django_extensions/js/jquery.ajaxQueue.js b/django_extensions/static/django_extensions/js/jquery.ajaxQueue.js index cd4492c13..aca15d9a6 100644 --- a/django_extensions/static/django_extensions/js/jquery.ajaxQueue.js +++ b/django_extensions/static/django_extensions/js/jquery.ajaxQueue.js @@ -1,8 +1,5 @@ /** * Ajax Queue Plugin - * - * Homepage: http://jquery.com/plugins/project/ajaxqueue - * Documentation: http://docs.jquery.com/AjaxQueue */ /** diff --git a/django_extensions/static/django_extensions/js/jquery.bgiframe.js b/django_extensions/static/django_extensions/js/jquery.bgiframe.js index 5c3735d68..1a452ba00 100644 --- a/django_extensions/static/django_extensions/js/jquery.bgiframe.js +++ b/django_extensions/static/django_extensions/js/jquery.bgiframe.js @@ -1,4 +1,4 @@ -/*! Copyright (c) 2010 Brandon Aaron (http://brandonaaron.net) +/*! Copyright (c) 2010 Brandon Aaron (http://brandon.aaron.sh/) * Licensed under the MIT License (LICENSE.txt). * * Version 2.1.2 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 38cb2af68..aadbb9973 100644 --- a/django_extensions/templates/django_extensions/graph_models/django2018/digraph.dot +++ b/django_extensions/templates/django_extensions/graph_models/django2018/digraph.dot @@ -6,7 +6,8 @@ {% block digraph_options %}fontname = "Roboto" fontsize = 8 splines = true - rankdir = "{{ rankdir }}"{% endblock %} + rankdir = "{{ rankdir }}" + {% if ordering %}ordering = "{{ ordering }}"{% endif %}{% endblock %} node [{% block node_options %} fontname = "Roboto" diff --git a/django_extensions/templates/django_extensions/graph_models/django2018/label.dot b/django_extensions/templates/django_extensions/graph_models/django2018/label.dot index 6303a806c..b88f50641 100644 --- a/django_extensions/templates/django_extensions/graph_models/django2018/label.dot +++ b/django_extensions/templates/django_extensions/graph_models/django2018/label.dot @@ -12,8 +12,8 @@ style="rounded"{% endif %} {% indentby 2 if use_subgraph %}{% for model in graph.models %} {{ model.app_name }}_{{ model.name }} [label=< - -
+ + diff --git a/django_extensions/templates/django_extensions/graph_models/django2018style/digraph.dot b/django_extensions/templates/django_extensions/graph_models/django2018style/digraph.dot new file mode 100644 index 000000000..e2a330611 --- /dev/null +++ b/django_extensions/templates/django_extensions/graph_models/django2018style/digraph.dot @@ -0,0 +1,27 @@ +{% block digraph %}digraph model_graph { + // Dotfile by Django-Extensions graph_models + // Created: {{ created_at }} + {% if cli_options %}// Cli Options: {{ cli_options }}{% endif %} + + {% block digraph_options %}fontname = "Roboto" + fontsize = 8 + splines = true + rankdir = "{{ rankdir }}"{% endblock %} + + node [{% block node_options %} + fontname = "Roboto" + fontsize = 8 + shape = "plaintext" + {% endblock %}] + + edge [{% block edge_options %} + fontname = "Roboto" + fontsize = 8 + {% endblock %}] + + // Labels +{% block labels %}{% for graph in graphs %}{% include "django_extensions/graph_models/django2018style/label.dot" %}{% endfor %}{% endblock %} + + // Relations +{% block relations %}{% for graph in graphs %}{% include "django_extensions/graph_models/django2018style/relation.dot" %}{% endfor %}{% endblock %} +}{% endblock %} diff --git a/django_extensions/templates/django_extensions/graph_models/django2018style/label.dot b/django_extensions/templates/django_extensions/graph_models/django2018style/label.dot new file mode 100644 index 000000000..6949682e7 --- /dev/null +++ b/django_extensions/templates/django_extensions/graph_models/django2018style/label.dot @@ -0,0 +1,39 @@ +{% load indent_text %}{% if use_subgraph %} subgraph {{ graph.cluster_app_name }} { + label=< +
{{ model.label }}{% if model.abstracts %}
<{{ model.abstracts|join:"," }}>{% endif %}
+ +
+ + {{ graph.app_name }} + +
+ > + color=olivedrab4 + style="rounded"{% endif %} +{% indentby 2 if use_subgraph %}{% for model in graph.models %} + {{ model.app_name }}_{{ model.name }} [label=< + + + {% if not disable_fields %}{% for field in model.fields %} + {% if disable_abstract_fields and field.abstract %} + {% else %} + + {% endif %} + {% endfor %}{% endif %} +
+ + {{ model.label }}{% if model.abstracts %}
<{{ model.abstracts|join:"," }}>{% endif %} +
+ {% if field.abstract %}{% endif %}{% if field.relation or field.primary_key %}{% endif %}{{ field.label }}{% if field.relation or field.primary_key %}{% endif %}{% if field.abstract %}{% endif %} + + {% if field.abstract %}{% endif %}{% if field.relation or field.primary_key %}{% endif %}{{ field.type }}{% if field.relation or field.primary_key %}{% endif %}{% if field.abstract %}{% endif %} +
+ >] +{% endfor %}{% endindentby %} +{% if use_subgraph %} }{% endif %} diff --git a/django_extensions/templates/django_extensions/graph_models/django2018style/relation.dot b/django_extensions/templates/django_extensions/graph_models/django2018style/relation.dot new file mode 100644 index 000000000..c5ef3d731 --- /dev/null +++ b/django_extensions/templates/django_extensions/graph_models/django2018style/relation.dot @@ -0,0 +1,10 @@ +{% for model in graph.models %}{% for relation in model.relations %}{% if relation.needs_node %} {{ relation.target_app }}_{{ relation.target }} [label=< + + +
+ {{ relation.target }} +
+ >]{% endif %} + {{ model.app_name }}_{{ model.name }} -> {{ relation.target_app }}_{{ relation.target }} + [label=" {{ relation.label }}"] {{ relation.arrows }}; +{% endfor %}{% endfor %} 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 ef4c36abf..874dc4bd6 100644 --- a/django_extensions/templates/django_extensions/graph_models/original/digraph.dot +++ b/django_extensions/templates/django_extensions/graph_models/original/digraph.dot @@ -6,7 +6,8 @@ {% block digraph_options %}fontname = "Helvetica" fontsize = 8 splines = true - rankdir = "{{ rankdir }}"{% endblock %} + rankdir = "{{ rankdir }}" + {% if ordering %}ordering = "{{ ordering }}"{% endif %}{% endblock %} node [{% block node_options %} fontname = "Helvetica" diff --git a/django_extensions/templates/django_extensions/graph_models/original/label.dot b/django_extensions/templates/django_extensions/graph_models/original/label.dot index 6db9980df..a46fbbe12 100644 --- a/django_extensions/templates/django_extensions/graph_models/original/label.dot +++ b/django_extensions/templates/django_extensions/graph_models/original/label.dot @@ -12,8 +12,8 @@ style="rounded"{% endif %} {% indentby 2 if use_subgraph %}{% for model in graph.models %} {{ model.app_name }}_{{ model.name }} [label=< - -
+ + diff --git a/django_extensions/templatetags/debugger_tags.py b/django_extensions/templatetags/debugger_tags.py index 93f2ddce9..67d7e1436 100644 --- a/django_extensions/templatetags/debugger_tags.py +++ b/django_extensions/templatetags/debugger_tags.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ Make debugging Django templates easier. @@ -19,19 +18,19 @@ @register.filter def ipdb(obj): # pragma: no cover """Interactive Python debugger filter.""" - __import__('ipdb').set_trace() + __import__("ipdb").set_trace() return obj @register.filter def pdb(obj): """Python debugger filter.""" - __import__('pdb').set_trace() + __import__("pdb").set_trace() return obj @register.filter def wdb(obj): # pragma: no cover """Web debugger filter.""" - __import__('wdb').set_trace() + __import__("wdb").set_trace() return obj diff --git a/django_extensions/templatetags/highlighting.py b/django_extensions/templatetags/highlighting.py index a38d39d73..6153e0069 100644 --- a/django_extensions/templatetags/highlighting.py +++ b/django_extensions/templatetags/highlighting.py @@ -1,17 +1,13 @@ -# -*- coding: utf-8 -*- """ Similar to syntax_color.py but this is intended more for being able to copy+paste actual code into your Django templates without needing to escape or anything crazy. -http://lobstertech.com/2008/aug/30/django_syntax_highlight_template_tag/ - Example: {% load highlighting %}
{{ model.label }}{% if model.abstracts %}
<{{ model.abstracts|join:"," }}>{% endif %}