Repository: django-extensions/django-extensions
Pull request #1628 (2021-02-12 21:37:50)

fix shell_plus --command globals / locals error
When running a "more complex" command via `--command` shell_plus has historically passed an empty globals and the imports as locals. This will cause Python to act strangely for certain inputs such as list comprehensions. This was fixed in Django's shell command [1] and by passing the imports as the only arg Python will use them as both the locals and the globals which is as if the `exec` was happening at the module level.

[1]: https://code.djangoproject.com/ticket/32183

See also: https://stackoverflow.com/questions/45132645/list-comprehension-in-exec-with-empty-locals-nameerror

coveralls: 
[![Coverage Status](https://coveralls.io/builds/37101957/badge)](https://coveralls.io/builds/37101957)

Coverage decreased (-71.1%) to 0.0% when pulling **c2e6191b09b8f353ae88cd52373e6ee798899ab8 on terencehonles:fix-shell_plus-command** into **f3357da0c66096d5bacf7ac6228c18001dabd7ef on django-extensions:master**.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/37101957/badge)](https://coveralls.io/builds/37101957)

Coverage decreased (-71.1%) to 0.0% when pulling **c2e6191b09b8f353ae88cd52373e6ee798899ab8 on terencehonles:fix-shell_plus-command** into **f3357da0c66096d5bacf7ac6228c18001dabd7ef on django-extensions:master**.


Patch:
diff --git a/.github/workflows/compile_catalog.yml b/.github/workflows/compile_catalog.yml
new file mode 100644
index 00000000..5e408784
--- /dev/null
+++ b/.github/workflows/compile_catalog.yml
@@ -0,0 +1,24 @@
+name: Compile Catalog
+
+on:
+  pull_request:
+  push:
+    branches:
+      - main
+
+jobs:
+  compile_catalog:
+    name: Compile Catalog
+    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.9
+      - run: python -m pip install tox
+      - name: Compile Catalog
+        run: tox
+        env:
+          TOXENV: compile-catalog
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
new file mode 100644
index 00000000..c2006298
--- /dev/null
+++ b/.github/workflows/linters.yml
@@ -0,0 +1,40 @@
+name: Linters
+
+on:
+  pull_request:
+  push:
+    branches:
+      - main
+
+jobs:
+  flake8:
+    name: flake8
+    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.9
+      - run: python -m pip install tox
+      - name: tox py39-flake8
+        run: tox
+        env:
+          TOXENV: py39-flake8
+
+  mypy:
+    name: mypy
+    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.9
+      - run: python -m pip install tox
+      - name: tox mypy
+        run: tox
+        env:
+          TOXENV: mypy
diff --git a/.github/workflows/precommit.yml b/.github/workflows/precommit.yml
new file mode 100644
index 00000000..85caad92
--- /dev/null
+++ b/.github/workflows/precommit.yml
@@ -0,0 +1,24 @@
+name: Pre-Commit Checks
+
+on:
+  pull_request:
+  push:
+    branches:
+      - main
+
+jobs:
+  precommit:
+    name: precommit
+    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.9
+      - run: python -m pip install tox
+      - name: tox precommit
+        run: tox
+        env:
+          TOXENV: precommit
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
new file mode 100644
index 00000000..de6fa94e
--- /dev/null
+++ b/.github/workflows/security.yml
@@ -0,0 +1,24 @@
+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.9
+      - run: python -m pip install tox
+      - name: safety
+        run: tox
+        env:
+          TOXENV: safety
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ce9c9e16..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,162 +0,0 @@
-language: python
-os: linux
-
-jobs:
-  fast_finish: true
-  include:
-    # Misc
-    - python: 3.9
-      env: TOXENV=py39-flake8
-    - python: 3.9
-      env: TOXENV=precommit
-    - python: 3.9
-      env: TOXENV=safety
-    - python: 3.9
-      env: TOXENV=mypy
-    - python: 3.9
-      env: TOXENV=compile-catalog
-    # Django 2.2: 3.6, 3.7, 3.8 or 3.9 / PyPy 3.x
-    - python: 3.6
-      env: TOXENV=py36-dj22
-    - python: 3.7
-      env: TOXENV=py37-dj22
-    - python: 3.7
-      env: TOXENV=py37-dj22-postgres
-      services:
-        - postgresql
-    - python: 3.7
-      env: TOXENV=py37-dj22-mysql
-      services:
-        - mysql
-    - python: 3.8
-      env: TOXENV=py38-dj22
-    - python: 3.8
-      env: TOXENV=py38-dj22-postgres
-      services:
-        - postgresql
-    - python: 3.8
-      env: TOXENV=py38-dj22-mysql
-      services:
-        - mysql
-    - python: 3.9
-      env: TOXENV=py38-dj22
-    - python: 3.9
-      env: TOXENV=py38-dj22-postgres
-      services:
-        - postgresql
-    - python: 3.9
-      env: TOXENV=py38-dj22-mysql
-      services:
-        - mysql
-    - python: pypy3
-      env: TOXENV=pypy3-dj22
-    # Django 3.0: Python 3.6, 3.7 or 3.8 / PyPy 3.x
-    - python: 3.6
-      env: TOXENV=py36-dj30
-    - python: 3.7
-      env: TOXENV=py37-dj30
-    - python: 3.7
-      env: TOXENV=py37-dj30-postgres
-      services:
-        - postgresql
-    - python: 3.7
-      env: TOXENV=py37-dj30-mysql
-      services:
-        - mysql
-    - python: 3.8
-      env: TOXENV=py38-dj30
-    - python: 3.8
-      env: TOXENV=py38-dj30-postgres
-      services:
-        - postgresql
-    - python: 3.8
-      env: TOXENV=py38-dj30-mysql
-      services:
-        - mysql
-    - python: pypy3
-      env: TOXENV=pypy3-dj30
-    # Django 3.1: Python 3.6, 3.7, 3.8 or 3.9 / PyPy 3.x
-    - python: 3.6
-      env: TOXENV=py36-dj31
-    - python: 3.7
-      env: TOXENV=py37-dj31
-    - python: 3.7
-      env: TOXENV=py37-dj31-postgres
-      services:
-        - postgresql
-    - python: 3.7
-      env: TOXENV=py37-dj31-mysql
-      services:
-        - mysql
-    - python: 3.8
-      env: TOXENV=py38-dj31
-    - python: 3.8
-      env: TOXENV=py38-dj31-postgres
-      services:
-        - postgresql
-    - python: 3.8
-      env: TOXENV=py38-dj31-mysql
-      services:
-        - mysql
-    - python: 3.9
-      env: TOXENV=py38-dj31
-    - python: 3.9
-      env: TOXENV=py38-dj31-postgres
-      services:
-        - postgresql
-    - python: 3.9
-      env: TOXENV=py38-dj31-mysql
-      services:
-        - mysql
-    - python: pypy3
-      env: TOXENV=pypy3-dj31
-    # Django master: Python 3.6, 3.7, 3.8 or 3.9 / PyPy 3.x
-    - python: 3.6
-      env: TOXENV=py36-djmaster
-    - python: 3.7
-      env: TOXENV=py37-djmaster
-    - python: 3.7
-      env: TOXENV=py37-djmaster-postgres
-      services:
-        - postgresql
-    - python: 3.8
-      env: TOXENV=py38-djmaster
-    - python: 3.8
-      env: TOXENV=py38-djmaster-postgres
-      services:
-        - postgresql
-    - python: 3.9
-      env: TOXENV=py38-djmaster
-    - python: 3.9
-      env: TOXENV=py38-djmaster-postgres
-      services:
-        - postgresql
-    - python: pypy3
-      env: TOXENV=pypy3-djmaster
-
-  allow_failures:
-    - python: 3.6
-      env: TOXENV=py36-djmaster
-    - python: 3.7
-      env: TOXENV=py37-djmaster
-    - python: 3.7
-      env: TOXENV=py37-djmaster-postgres
-    - python: 3.8
-      env: TOXENV=py38-djmaster
-    - python: 3.8
-      env: TOXENV=py38-djmaster-postgres
-    - python: 3.9
-      env: TOXENV=py39-djmaster
-    - python: 3.9
-      env: TOXENV=py39-djmaster-postgres
-    - python: pypy3
-      env: TOXENV=pypy3-djmaster
-
-install:
-  - pip install virtualenv tox coveralls
-
-script:
-  - tox
-
-after_success:
-  - coveralls
diff --git a/README.rst b/README.rst
index 376d2a0a..784d095f 100644
--- a/README.rst
+++ b/README.rst
@@ -5,9 +5,20 @@
 .. image:: https://img.shields.io/pypi/l/django-extensions.svg
    :target: https://raw.githubusercontent.com/django-extensions/django-extensions/master/LICENSE
 
-.. image:: https://secure.travis-ci.org/django-extensions/django-extensions.svg?branch=master
-    :alt: Build Status
-    :target: http://travis-ci.org/django-extensions/django-extensions
+.. image:: https://github.com/django-extensions/django-extensions/actions/workflows/compile_catalog.yml/badge.svg
+    :target: https://github.com/django-extensions/django-extensions/actions
+
+.. image:: https://github.com/django-extensions/django-extensions/actions/workflows/linters.yml/badge.svg
+    :target: https://github.com/django-extensions/django-extensions/actions
+
+.. image:: https://github.com/django-extensions/django-extensions/actions/workflows/precommit.yml/badge.svg
+    :target: https://github.com/django-extensions/django-extensions/actions
+
+.. image:: https://github.com/django-extensions/django-extensions/actions/workflows/pytest.yml/badge.svg
+    :target: https://github.com/django-extensions/django-extensions/actions
+
+.. image:: https://github.com/django-extensions/django-extensions/actions/workflows/security.yml/badge.svg
+    :target: https://github.com/django-extensions/django-extensions/actions
 
 .. image:: https://img.shields.io/pypi/v/django-extensions.svg
     :target: https://pypi.python.org/pypi/django-extensions/
diff --git a/django_extensions/management/commands/shell_plus.py b/django_extensions/management/commands/shell_plus.py
index a8594e7f..c5a97282 100644
--- a/django_extensions/management/commands/shell_plus.py
+++ b/django_extensions/management/commands/shell_plus.py
@@ -204,7 +204,7 @@ class Command(BaseCommand):
 
         return {'django_extensions': ks}
 
-    def run_notebookapp(self, app_init, options, use_kernel_specs=True):
+    def run_notebookapp(self, app_init, options, use_kernel_specs=True, history=True):
         no_browser = options['no_browser']
 
         if self.extra_args:
@@ -235,6 +235,10 @@ class Command(BaseCommand):
         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 callable(app_init):
             app = app_init
             warnings.warn('Initialize should be a callable not an app instance', DeprecationWarning)
@@ -322,7 +326,7 @@ class Command(BaseCommand):
                 return app
 
         def run_jupyterlab():
-            self.run_notebookapp(app_init, options)
+            self.run_notebookapp(app_init, options, history=False)
         return run_jupyterlab
 
     @shell_runner(flags=['--plain'], name='plain Python')
@@ -571,7 +575,7 @@ for k, m in shells.import_objects({}, no_style()).items():
 
             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/docs/shell_plus.rst b/docs/shell_plus.rst
index ae1954a1..e2545157 100644
--- a/docs/shell_plus.rst
+++ b/docs/shell_plus.rst
@@ -179,11 +179,11 @@ Default collision resolver. Model from last application in alphabetical order is
 
 **InstalledAppsOrderCR**
 
-Collision resolver which selects first model from INSTALLED_APPS.
+Collision resolver which selects the first model from INSTALLED_APPS.
 You can set your own app priorities list 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::
+This collision resolver will select a model from the first app on this list.
+If both app's are absent on this list, resolver will choose a model from the first app in alphabetical order::
 
     from programming import Language
 
@@ -220,25 +220,25 @@ Model from last application in alphabetical order is selected::
 
 Collision resolver which is mixin of AppNamePrefixCR and InstalledAppsOrderCR.
 
-In case of collisions he sets aliases like AppNamePrefixCR, but sets default model using InstalledAppsOrderCR::
+In case of collisions he sets aliases like AppNamePrefixCR, but sets the default model using InstalledAppsOrderCR::
 
     from programming import Language, Language (as programming_Language)
     from workers import Language (as workers_Language)
 
 **AppNameSuffixCustomOrderCR**
 
-Collision resolver which is mixin of AppNameSuffixCR and InstalledAppsOrderCR.
+Collision resolver which is a mixin of AppNameSuffixCR and InstalledAppsOrderCR.
 
-In case of collisions he sets aliases like AppNameSuffixCR, but sets default model using InstalledAppsOrderCR::
+In case of collisions he sets aliases like AppNameSuffixCR, but sets the default model using InstalledAppsOrderCR::
 
     from programming import Language, Language (as Language_programming)
     from workers import Language (as Language_workers)
 
 **FullPathCustomOrderCR**
 
-Collision resolver which is mixin of FullPathCR and InstalledAppsOrderCR.
+Collision resolver which is a mixin of FullPathCR and InstalledAppsOrderCR.
 
-In case of collisions he sets aliases like FullPathCR, but sets default model using InstalledAppsOrderCR::
+In case of collisions he sets aliases like FullPathCR, but sets the default model using InstalledAppsOrderCR::
 
     from programming import Language, Language (as programming_Language)
     from workers import Language (as workers_Language)
@@ -247,7 +247,7 @@ In case of collisions he sets aliases like FullPathCR, but sets default model us
 
 Collision resolver which transform pair (app_label, model_name) to alias ``{app_label}_{model_name}``
 
-This is very similar to ``AppNamePrefixCR`` but this may generate shorter names in case of apps nested
+This is very similar to ``AppNamePrefixCR`` but this may generate shorter names in the case of apps nested
 into several namespace (like Django's auth app)::
 
     # with AppNamePrefixCR
@@ -281,7 +281,7 @@ Abstract resolver which transforms full model name into alias.
 To use him you need to overwrite transform_import function
 which should have one parameter.
 
-It will be full model name. It should return valid alias as str instance.
+It will be a full model name. It should return valid alias as a str instance.
 
 **AppNameCR**
 
@@ -292,13 +292,13 @@ app_name and model_name. For example: ``{app_name}_{model_name}``.
 
 Model from last application in alphabetical order is selected.
 
-You can mix PathBasedCR or AppNameCR with InstalledAppsOrderCR, but InstalledAppsOrderCR should be second base class.
+You can mix PathBasedCR or AppNameCR with InstalledAppsOrderCR, but InstalledAppsOrderCR should be the second base class.
 
 **BaseCR**
 
 Abstract base collision resolver. All collision resolvers needs to inherit from this class.
 
-To write custom collision resolver you need to overwrite resolve_collisions function.
+To write a custom collision resolver you need to overwrite the resolve_collisions function.
 It receives ``Dict[str, List[str]]`` where key is model name and values are full model names
 (full model name means: module + model_name).
 
@@ -309,14 +309,14 @@ Import Subclasses
 If you want to load automatically all project subclasses of some base class,
 you can achieve this by setting ``SHELL_PLUS_SUBCLASSES_IMPORT`` option.
 
-It must be list of either classes or strings containing paths to this classes.
+It must be a list of either classes or strings containing paths to these classes.
 
-For example if you want to load all your custom managers than you should provide::
+For example, if you want to load all your custom managers then you should provide::
 
     from django.db.models import Manager
     SHELL_PLUS_SUBCLASSES_IMPORT = [Manager]
 
-Than shell_plus will load all your custom managers::
+Then shell_plus will load all your custom managers::
 
     # Shell Plus Subclasses Imports
     from utils.managers import AbstractManager
@@ -324,7 +324,7 @@ Than shell_plus will load all your custom managers::
     from somewhere.else import MyOtherManager
     # django.db.models.Manager is not loaded because only project classes are.
 
-By default all subclasses of your base class from all projects module will be loaded.
+By default, all subclasses of your base class from all projects modules will be loaded.
 
 You can exclude some modules and all their submodules by passing ``SHELL_PLUS_SUBCLASSES_IMPORT_MODULES_BLACKLIST`` option::
 
@@ -335,7 +335,7 @@ If these modules are excluded only ``MyCustomManager`` from ``myapp.managers`` w
 
 If you are using ``SHELL_PLUS_SUBCLASSES_IMPORT`` shell_plus loads all project modules for finding subclasses.
 
-Sometimes it can lead to some errors(for example when we have old unused module which contains syntax errors).
+Sometimes it can lead to some errors(for example when we have an old unused module which contains syntax errors).
 
 Excluding these modules can help avoid shell_plus crashes in some situations.
 It is recommended to exclude all ``setup.py`` files.
@@ -389,10 +389,10 @@ Note that the IPython Notebook feature doesn't currently honor the
 Additional Imports
 ------------------
 
-In addition to importing the models you can specify other items to import by default.
+In addition to importing the models, you can specify other items to import by default.
 These can be specified with the settings ``SHELL_PLUS_IMPORTS``, ``SHELL_PLUS_PRE_IMPORTS`` and ``SHELL_PLUS_POST_IMPORTS``.
 
-The other of import loading is as follows:
+The order of import loading is as follows:
 
  - ``SHELL_PLUS_PRE_IMPORTS``
  - Subclasses (if enabled)
diff --git a/setup.py b/setup.py
index be722bb9..a9301ac5 100644
--- a/setup.py
+++ b/setup.py
@@ -107,7 +107,7 @@ setup(
     cmdclass=cmdclasses,
     package_data=package_data,
     python_requires=">=3.6",
-    install_requires=[],
+    install_requires=["Django>=2.2"],
     extras_require={},
     classifiers=[
         'Development Status :: 5 - Production/Stable',
diff --git a/tox.ini b/tox.ini
index 02e6db89..43bf913f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,6 +24,9 @@ passenv =
     DJANGO_EXTENSIONS_DATABASE_ENGINE
     DJANGO_EXTENSIONS_DATABASE_NAME
     DJANGO_EXTENSIONS_DATABASE_USER
+    DJANGO_EXTENSIONS_DATABASE_PASSWORD
+    DJANGO_EXTENSIONS_DATABASE_HOST
+    DJANGO_EXTENSIONS_DATABASE_PORT
 
 setenv =
     postgres: DJANGO_EXTENSIONS_DATABASE_ENGINE = {env:DJANGO_EXTENSIONS_DATABASE_ENGINE:django.db.backends.postgresql}
@@ -36,7 +39,7 @@ deps =
     dj22: Django==2.2
     dj30: Django>=3.0,<3.1
     dj31: Django>=3.1,<3.2
-    djmaster: https://github.com/django/django/archive/master.tar.gz
+    djmaster: https://github.com/django/django/archive/refs/heads/main.zip
     postgres: psycopg2-binary
     mysql: mysqlclient
 


Test patch:
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
new file mode 100644
index 00000000..547af9ad
--- /dev/null
+++ b/.github/workflows/pytest.yml
@@ -0,0 +1,76 @@
+name: PyTest
+
+on:
+  pull_request:
+  push:
+    branches:
+      - main
+
+jobs:
+  pytest:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      max-parallel: 4
+      matrix:
+        python-version:
+          - 3.6
+          - 3.7
+          - 3.8
+          - 3.9
+          - pypy3
+        tox-django-version:
+          - "22"
+          - "30"
+          - "31"
+          # GH Actions don't support something like allow-failure ?
+          # - "master"
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - run: python -m pip install tox
+      - name: Pytest
+        run: tox -e py-dj${{ matrix.tox-django-version }}
+
+  pytest-postgresql:
+    runs-on: ubuntu-latest
+    services:
+      postgres:
+        image: postgres
+        env:
+          POSTGRES_USER: postgres
+          POSTGRES_PASSWORD: postgres
+          POSTGRES_DB: django_extensions_test
+        ports:
+          - 5432:5432
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-timeout 5s
+          --health-retries 5
+    strategy:
+      fail-fast: false
+      max-parallel: 4
+      matrix:
+        python-version:
+          - 3.9
+        tox-django-version:
+          - "31"
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - run: python -m pip install tox
+      - name: Pytest
+        run: tox -e py-dj${{ matrix.tox-django-version }}-postgres
+        env:
+          DJANGO_EXTENSIONS_DATABASE_HOST: localhost
+          DJANGO_EXTENSIONS_DATABASE_USER: postgres
+          DJANGO_EXTENSIONS_DATABASE_PASSWORD: postgres
diff --git a/django_extensions/management/commands/drop_test_database.py b/django_extensions/management/commands/drop_test_database.py
index 0e058db7..be2089dc 100644
--- a/django_extensions/management/commands/drop_test_database.py
+++ b/django_extensions/management/commands/drop_test_database.py
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
+from itertools import count
 import os
 import logging
 import warnings
@@ -82,12 +83,15 @@ class Command(BaseCommand):
         verbosity = options["verbosity"]
         if options['interactive']:
             confirm = input("""
-You have requested to drop the test database.
+You have requested to drop all test databases.
 This will IRREVERSIBLY DESTROY
-ALL data in the database "%s".
+ALL data in the database "{db_name}"
+and all cloned test databases generated via
+the "--parallel" flag (these are sequentially
+named "{db_name}_1", "{db_name}_2", etc.).
 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: """.format(db_name=database_name))
         else:
             confirm = 'yes'
 
@@ -95,13 +99,45 @@ Type 'yes' to continue, or 'no' to cancel: """ % (database_name,))
             print("Reset cancelled.")
             return
 
+        def get_database_names(formatter):
+            """
+            Return a generator of all possible test database names.
+            e.g., 'test_foo', 'test_foo_1', test_foo_2', etc.
+
+            formatter: func returning a clone db name given the primary db name
+            and the clone's number, e.g., 'test_foo_1' for mysql/postgres, and
+            'test_foo_1..sqlite3' for sqlite (re: double dots, see comments).
+            """
+            yield database_name
+            yield from (formatter(database_name, n) for n in count(1))
+
         if engine in SQLITE_ENGINES:
+            # By default all sqlite test databases are created in memory.
+            # There will only be database files to delete if the developer has
+            # specified a test database name, which forces files to be written
+            # to disk.
+
+            logging.info("Unlinking %s databases" % engine)
+
+            def format_filename(name, number):
+                filename, ext = os.path.splitext(name)
+                # Since splitext() includes the dot in 'ext', the inclusion of
+                # the dot in the format string below is incorrect and creates a
+                # double dot. Django makes this mistake, so it must be
+                # replicated here. If fixed in Django, this code should be
+                # updated accordingly.
+                # Reference: https://code.djangoproject.com/ticket/32582
+                return '{}_{}.{}'.format(filename, number, ext)
+
             try:
-                logging.info("Unlinking %s database" % engine)
-                if os.path.isfile(database_name):
-                    os.unlink(database_name)
+                for db_name in get_database_names(format_filename):
+                    if not os.path.isfile(db_name):
+                        break
+                    logging.info('Unlinking database named "%s"' % db_name)
+                    os.unlink(db_name)
             except OSError:
                 return
+
         elif engine in MYSQL_ENGINES:
             import MySQLdb as Database
             kwargs = {
@@ -117,9 +153,19 @@ Type 'yes' to continue, or 'no' to cancel: """ % (database_name,))
                 kwargs['port'] = int(database_port)
 
             connection = Database.connect(**kwargs)
-            drop_query = 'DROP DATABASE IF EXISTS `%s`' % database_name
-            logging.info('Executing: "' + drop_query + '"')
-            connection.query(drop_query)
+            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
+                row_count = cursor.execute(exists_query)
+                if row_count < 1:
+                    break
+                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
 
@@ -136,14 +182,22 @@ Type 'yes' to continue, or 'no' to cancel: """ % (database_name,))
             connection = Database.connect(**conn_params)
             connection.set_isolation_level(0)  # autocommit false
             cursor = connection.cursor()
-            drop_query = "DROP DATABASE IF EXISTS \"%s\";" % database_name
-            logging.info('Executing: "' + drop_query + '"')
 
-            try:
-                cursor.execute(drop_query)
-            except Database.ProgrammingError as e:
-                logging.exception("Error: %s" % str(e))
-                return
+            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
+                    if cursor.rowcount < 1:
+                        break
+                    drop_query = "DROP DATABASE IF EXISTS \"%s\";" % db_name
+                    logging.info('Executing: "' + drop_query + '"')
+                    cursor.execute(drop_query)
+                except Database.ProgrammingError as e:
+                    logging.exception("Error: %s" % str(e))
+                    return
         else:
             raise CommandError("Unknown database engine %s" % engine)
 
diff --git a/tests/management/commands/shell_plus_tests/test_shell_plus.py b/tests/management/commands/shell_plus_tests/test_shell_plus.py
index 63e13609..be59e900 100644
--- a/tests/management/commands/shell_plus_tests/test_shell_plus.py
+++ b/tests/management/commands/shell_plus_tests/test_shell_plus.py
@@ -4,7 +4,7 @@ import re
 import pytest
 import inspect
 
-from io import StringIO
+from textwrap import dedent
 from django.core.management import call_command
 from django.db.models import Model
 from django.test import override_settings
@@ -12,10 +12,20 @@ from django.test import override_settings
 from django_extensions.management.commands import shell_plus
 
 
+def test_shell_plus_command(capsys):
+    script = dedent('''\
+        def _fn(x): return x * 2
+        print([_fn(i) for i in range(10)])
+        ''')
+    call_command("shell_plus", "--command=" + script)
+    out, err = capsys.readouterr()
+
+    assert out.rstrip().endswith(repr([i * 2 for i in range(10)]))
+
+
 @pytest.mark.django_db()
 @override_settings(SHELL_PLUS_SQLPARSE_ENABLED=False, SHELL_PLUS_PYGMENTS_ENABLED=False)
 def test_shell_plus_print_sql(capsys):
-    out = StringIO()
     try:
         from django.db import connection
         from django.db.backends import utils
diff --git a/tests/management/commands/test_drop_test_database.py b/tests/management/commands/test_drop_test_database.py
index a7666bf9..ed8d9b11 100644
--- a/tests/management/commands/test_drop_test_database.py
+++ b/tests/management/commands/test_drop_test_database.py
@@ -1,11 +1,69 @@
 # -*- coding: utf-8 -*-
 from io import StringIO
+from unittest.mock import MagicMock, Mock, PropertyMock, call, patch
 
 from django.core.management import CommandError, call_command
 from django.test import TestCase
 from django.test.utils import override_settings
 
-from unittest.mock import MagicMock, Mock, patch
+
+# Database testing configurations
+
+UNKOWN_ENGINE = {
+    'default': {
+        'ENGINE': 'django.db.backends.unknown',
+        'NAME': 'unknown',
+    }
+}
+
+NO_TEST_NAME = {
+    'default': {
+        'ENGINE': 'django.db.backends.mysql',
+        'NAME': 'test',
+        'TEST': {
+            'NAME': '',
+        }
+    }
+}
+
+SQLITE = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': 'db.sqlite3',
+    }
+}
+
+MYSQL_HOST_PORT = {
+    'default': {
+        'ENGINE': 'django.db.backends.mysql',
+        'NAME': 'test',
+        'USER': 'test',
+        'PASSWORD': 'test',
+        'HOST': 'localhost',
+        'PORT': '3306',
+    },
+}
+
+MYSQL_SOCKET = {
+    'default': {
+        'ENGINE': 'django.db.backends.mysql',
+        'NAME': 'test',
+        'USER': 'test',
+        'PASSWORD': 'test',
+        'HOST': '/var/run/mysqld/mysql.sock',
+    },
+}
+
+POSTGRES = {
+    'default': {
+        'ENGINE': 'django.db.backends.postgresql_psycopg2',
+        'NAME': 'test',
+        'USER': 'test',
+        'PASSWORD': 'test',
+        'PORT': '5432',
+        'HOST': 'localhost',
+    },
+}
 
 
 class DropTestDatabaseExceptionsTests(TestCase):
@@ -15,28 +73,15 @@ class DropTestDatabaseExceptionsTests(TestCase):
         with self.assertRaisesRegex(CommandError, "Unknown database unknown"):
             call_command('drop_test_database', '--database=unknown')
 
-    @override_settings(DATABASES={
-        'default': {
-            'ENGINE': 'django.db.backends.unknown',
-            'NAME': 'unknown',
-        }
-    })
+    @override_settings(DATABASES=UNKOWN_ENGINE)
     @patch('django_extensions.management.commands.drop_test_database.input')
     def test_should_raise_CommandError_if_unknown_database_engine(self, m_input):
         m_input.return_value = 'yes'
         with self.assertRaisesRegex(CommandError, "Unknown database engine django.db.backends.unknown"):
             call_command('drop_test_database')
 
-    @override_settings(DATABASES={
-        'default': {
-            'ENGINE': 'django.db.backends.mysql',
-            'NAME': 'test',
-            'TEST': {
-                'NAME': '',
-            }
-        }
-    })
-    def test_shoul_raise_CommandError_if_test_database_name_is_empty(self):
+    @override_settings(DATABASES=NO_TEST_NAME)
+    def test_should_raise_CommandError_if_test_database_name_is_empty(self):
         with self.assertRaisesRegex(CommandError, "You need to specify DATABASE_NAME in your Django settings file."):
             call_command('drop_test_database')
 
@@ -53,28 +98,64 @@ class DropTestDatabaseTests(TestCase):
 
         self.assertEqual("Reset cancelled.\n", m_stdout.getvalue())
 
-    @override_settings(DATABASES={
-        'default': {
-            'ENGINE': 'django.db.backends.sqlite3',
-            'NAME': 'db.sqlite3',
-        }
-    })
+    @override_settings(DATABASES=SQLITE)
     @patch('sys.stdout', new_callable=StringIO)
     @patch('os.path.isfile')
     @patch('os.unlink')
-    def test_sqlite3_should_unlink_database(self, m_unlink, m_isfile, m_stdout):
-        m_isfile.return_value = True
+    def test_sqlite3_should_unlink_primary_test_database(self, m_unlink, m_isfile, m_stdout):
+        # Indicate that no clone databases exist
+        m_isfile.side_effect = (True, False)
         call_command('drop_test_database', '--noinput', verbosity=2)
 
-        m_unlink.assert_called_once_with('test_db.sqlite3')
-        self.assertIn("Reset successful.", m_stdout.getvalue())
+        with self.subTest('Should check for test database names until failure'):
+            self.assertListEqual(
+                m_isfile.call_args_list,
+                # See production code comments regarding double dots
+                [call('test_db.sqlite3'), call('test_db_1..sqlite3')],
+            )
 
-    @override_settings(DATABASES={
-        'default': {
-            'ENGINE': 'django.db.backends.sqlite3',
-            'NAME': 'db.sqlite3',
-        }
-    })
+        with self.subTest('Should unlink only primary test database'):
+            self.assertListEqual(
+                m_unlink.call_args_list,
+                [call('test_db.sqlite3')],
+            )
+
+        with self.subTest('Should report successful message'):
+            self.assertIn("Reset successful.", m_stdout.getvalue())
+
+    @override_settings(DATABASES=SQLITE)
+    @patch('os.path.isfile')
+    @patch('os.unlink')
+    def test_sqlite3_should_unlink_all_existing_clone_databases(self, m_unlink, m_isfile):
+        """Test cloned test databases created via 'manage.py test --parallel'."""
+        # Indicate that clone databases exist up to test_db_2.sqlite3
+        m_isfile.side_effect = (True, True, True, False)
+        call_command('drop_test_database', '--noinput')
+
+        with self.subTest('Should check for test database names until failure'):
+            self.assertListEqual(
+                m_isfile.call_args_list,
+                [
+                    call('test_db.sqlite3'),
+                    # See production code comments regarding double dots
+                    call('test_db_1..sqlite3'),
+                    call('test_db_2..sqlite3'),
+                    call('test_db_3..sqlite3'),
+                ],
+            )
+
+        with self.subTest('Should unlink all existing test databases'):
+            self.assertListEqual(
+                m_unlink.call_args_list,
+                [
+                    call('test_db.sqlite3'),
+                    # See production code comments regarding double dots
+                    call('test_db_1..sqlite3'),
+                    call('test_db_2..sqlite3'),
+                ],
+            )
+
+    @override_settings(DATABASES=SQLITE)
     @patch('sys.stdout', new_callable=StringIO)
     @patch('os.path.isfile')
     @patch('os.unlink')
@@ -85,86 +166,135 @@ class DropTestDatabaseTests(TestCase):
 
         self.assertNotIn("Reset successful.", m_stdout.getvalue())
 
-    @override_settings(DATABASES={
-        'default': {
-            'ENGINE': 'django.db.backends.mysql',
-            'NAME': 'test',
-            'USER': 'test',
-            'PASSWORD': 'test',
-            'HOST': 'localhost',
-            'PORT': '3306',
-        },
-    })
+    @override_settings(DATABASES=MYSQL_HOST_PORT)
     @patch('sys.stdout', new_callable=StringIO)
-    def test_mysql_should_drop_datatabase_with_host_and_port(self, m_stdout):
+    def test_mysql_should_drop_database_with_host_and_port(self, m_stdout):
         m_database = MagicMock()
-        m_connection = Mock()
-        m_database.connect.return_value = m_connection
+        # Indicate that no clone databases exist
+        # DROP queries return None while SELECT queries return a row count
+        m_database.connect.return_value.cursor.return_value.execute.side_effect = (1, None, 0)
 
         with patch.dict("sys.modules", MySQLdb=m_database):
             call_command('drop_test_database', '--noinput', verbosity=2)
 
-        m_connection.query.assert_called_with(
-            'DROP DATABASE IF EXISTS `test_test`')
+        with self.subTest('Should check for and remove test database names until failure'):
+            exists_query = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME="
+            self.assertListEqual(
+                m_database.connect.return_value.cursor.return_value.execute.call_args_list,
+                [
+                    call(exists_query + "'test_test';"),
+                    call('DROP DATABASE IF EXISTS `test_test`'),
+                    call(exists_query + "'test_test_1';"),
+                ],
+            )
 
-        self.assertIn("Reset successful.", m_stdout.getvalue())
+        with self.subTest('Should report successful message'):
+            self.assertIn("Reset successful.", m_stdout.getvalue())
 
-    @override_settings(DATABASES={
-        'default': {
-            'ENGINE': 'django.db.backends.mysql',
-            'NAME': 'test',
-            'USER': 'test',
-            'PASSWORD': 'test',
-            'HOST': '/var/run/mysqld/mysql.sock',
-        },
-    })
+    @override_settings(DATABASES=MYSQL_SOCKET)
     @patch('sys.stdout', new_callable=StringIO)
-    def test_mysql_should_drop_datatabase_with_unix_socket(self, m_stdout):
+    def test_mysql_should_drop_database_with_unix_socket(self, m_stdout):
         m_database = MagicMock()
-        m_connection = Mock()
-        m_database.connect.return_value = m_connection
+        # Indicate that no clone databases exist
+        # DROP queries return None while SELECT queries return a row count
+        m_database.connect.return_value.cursor.return_value.execute.side_effect = (1, None, 0)
 
         with patch.dict("sys.modules", MySQLdb=m_database):
             call_command('drop_test_database', '--noinput', verbosity=2)
 
-        m_connection.query.assert_called_with(
-            'DROP DATABASE IF EXISTS `test_test`')
-
-        self.assertIn("Reset successful.", m_stdout.getvalue())
-
-    @override_settings(DATABASES={
-        'default': {
-            'ENGINE': 'django.db.backends.postgresql_psycopg2',
-            'NAME': 'test',
-            'USER': 'test',
-            'PASSWORD': 'test',
-            'PORT': '5432',
-            'HOST': 'localhost',
-        },
-    })
+        with self.subTest('Should check for and remove test database names until failure'):
+            exists_query = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME="
+            self.assertListEqual(
+                m_database.connect.return_value.cursor.return_value.execute.call_args_list,
+                [
+                    call(exists_query + "'test_test';"),
+                    call('DROP DATABASE IF EXISTS `test_test`'),
+                    call(exists_query + "'test_test_1';"),
+                ],
+            )
+
+        with self.subTest('Should report successful message'):
+            self.assertIn("Reset successful.", m_stdout.getvalue())
+
+    @override_settings(DATABASES=MYSQL_HOST_PORT)
+    def test_mysql_should_drop_all_existing_clone_databases(self):
+        """Test cloned test databases created via 'manage.py test --parallel'."""
+        m_database = MagicMock()
+        # Indicate that clone databases exist up to test_test_2
+        # DROP queries return None while SELECT queries return a row count
+        m_database.connect.return_value.cursor.return_value.execute.side_effect = \
+            (1, None, 1, None, 1, None, 0)
+
+        with patch.dict("sys.modules", MySQLdb=m_database):
+            call_command('drop_test_database', '--noinput')
+
+        exists_query = "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME="
+        self.assertListEqual(
+            m_database.connect.return_value.cursor.return_value.execute.call_args_list,
+            [
+                call(exists_query + "'test_test';"),
+                call('DROP DATABASE IF EXISTS `test_test`'),
+                call(exists_query + "'test_test_1';"),
+                call('DROP DATABASE IF EXISTS `test_test_1`'),
+                call(exists_query + "'test_test_2';"),
+                call('DROP DATABASE IF EXISTS `test_test_2`'),
+                call(exists_query + "'test_test_3';"),
+            ],
+        )
+
+    @override_settings(DATABASES=POSTGRES)
     @patch('sys.stdout', new_callable=StringIO)
     def test_postgresql_should_drop_database(self, m_stdout):
         m_database = MagicMock()
         m_cursor = Mock()
         m_database.connect.return_value.cursor.return_value = m_cursor
+        # Indicate that no clone databases exist
+        type(m_cursor).rowcount = PropertyMock(side_effect=(1, 0))
 
         with patch.dict("sys.modules", psycopg2=m_database):
             call_command('drop_test_database', '--noinput', verbosity=2)
 
-        m_cursor.execute.assert_called_once_with(
-            'DROP DATABASE IF EXISTS "test_test";')
-        self.assertIn("Reset successful.", m_stdout.getvalue())
-
-    @override_settings(DATABASES={
-        'default': {
-            'ENGINE': 'django.db.backends.postgresql_psycopg2',
-            'NAME': 'test',
-            'USER': 'test',
-            'PASSWORD': 'test',
-            'PORT': '5432',
-            'HOST': 'localhost',
-        },
-    })
+        with self.subTest('Should check for and remove test database names until failure'):
+            exists_query = "SELECT datname FROM pg_catalog.pg_database WHERE datname="
+            self.assertListEqual(
+                m_cursor.execute.call_args_list,
+                [
+                    call(exists_query + "'test_test';"),
+                    call('DROP DATABASE IF EXISTS "test_test";'),
+                    call(exists_query + "'test_test_1';"),
+                ],
+            )
+
+        with self.subTest('Should report successful message'):
+            self.assertIn("Reset successful.", m_stdout.getvalue())
+
+    @override_settings(DATABASES=POSTGRES)
+    def test_postgresql_should_drop_all_existing_cloned_databases(self):
+        """Test cloned test databases created via 'manage.py test --parallel'."""
+        m_database = MagicMock()
+        m_cursor = Mock()
+        m_database.connect.return_value.cursor.return_value = m_cursor
+        # Indicate that clone databases exist up to test_test_2
+        type(m_cursor).rowcount = PropertyMock(side_effect=(1, 1, 1, 0))
+
+        with patch.dict("sys.modules", psycopg2=m_database):
+            call_command('drop_test_database', '--noinput')
+
+        exists_query = "SELECT datname FROM pg_catalog.pg_database WHERE datname="
+        self.assertListEqual(
+            m_cursor.execute.call_args_list,
+            [
+                call(exists_query + "'test_test';"),
+                call('DROP DATABASE IF EXISTS "test_test";'),
+                call(exists_query + "'test_test_1';"),
+                call('DROP DATABASE IF EXISTS "test_test_1";'),
+                call(exists_query + "'test_test_2';"),
+                call('DROP DATABASE IF EXISTS "test_test_2";'),
+                call(exists_query + "'test_test_3';"),
+            ],
+        )
+
+    @override_settings(DATABASES=POSTGRES)
     @patch('sys.stdout', new_callable=StringIO)
     def test_postgresql_should_not_print_Reset_successful_when_exception_occured(self, m_stdout):
         m_database = MagicMock()
diff --git a/tests/templatetags/test_syntax_color.py b/tests/templatetags/test_syntax_color.py
index dc6529d1..0b04fc8c 100644
--- a/tests/templatetags/test_syntax_color.py
+++ b/tests/templatetags/test_syntax_color.py
@@ -63,13 +63,12 @@ class SyntaxColorTagTests(TestCase):
         content = """{% load syntax_color %}
 {{ code_string|colorize_table }}
 """
-        expected_result = '''<table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre>1</pre></div></td><td class="code"><div class="highlight"><pre><span></span><span class="nt">&lt;h1&gt;</span>TEST<span class="nt">&lt;/h1&gt;</span>
-</pre></div>
-</td></tr></table>'''
-
         result = Template(content).render(ctx)
 
-        self.assertHTMLEqual(result, expected_result)
+        self.assertIn('<table class="highlighttable">', result)
+        self.assertIn('<td class="linenos">', result)
+        self.assertIn('>1</', result)
+        self.assertIn('<span class="nt">&lt;h1&gt;</span>TEST<span class="nt">&lt;/h1&gt;</span>', result)
 
     def test_colorize_table_should_return_value_if_lexer_class_not_found(self):
         ctx = Context({'code_string': '<h1>TEST</h1>'})
diff --git a/tests/testapp/settings.py b/tests/testapp/settings.py
index 49244572..a8046748 100644
--- a/tests/testapp/settings.py
+++ b/tests/testapp/settings.py
@@ -34,6 +34,10 @@ DATABASES = {
     'default': {
         'ENGINE': os.environ.get('DJANGO_EXTENSIONS_DATABASE_ENGINE', 'django.db.backends.sqlite3'),
         'NAME': os.environ.get('DJANGO_EXTENSIONS_DATABASE_NAME', ':memory:'),
+        'USER': os.environ.get("DJANGO_EXTENSIONS_DATABASE_USER"),
+        'PASSWORD': os.environ.get("DJANGO_EXTENSIONS_DATABASE_PASSWORD"),
+        'HOST': os.environ.get('DJANGO_EXTENSIONS_DATABASE_HOST'),
+        'PORT': os.environ.get('DJANGO_EXTENSIONS_DATABASE_PORT'),
     }
 }