From bdf46b2d302f348ae2797600f488e94d446ad547 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 9 Dec 2020 10:16:52 +0100 Subject: [PATCH] Migrate to GitHub Actions. (#317) --- .github/workflows/release.yml | 40 +++++ .github/workflows/test.yml | 78 +++++++++ .travis.yml | 216 ------------------------ CONTRIBUTING.rst | 2 +- README.rst | 12 +- changelog.rst | 7 + contribute.json | 2 +- django_nose/__init__.py | 10 +- django_nose/fixture_tables.py | 39 ++--- django_nose/management/commands/test.py | 6 +- django_nose/plugin.py | 41 ++--- django_nose/runner.py | 188 +++++++++++---------- django_nose/testcases.py | 38 +++-- django_nose/tools.py | 44 ++--- django_nose/utils.py | 8 +- docs/conf.py | 57 ++++--- requirements.txt | 2 +- runtests.sh | 1 + setup.cfg | 8 +- setup.py | 91 +++++----- testapp/migrations/0001_initial.py | 49 +++--- testapp/models.py | 2 +- testapp/plugin_t/test_with_plugins.py | 1 + testapp/runtests.py | 7 +- testapp/settings.py | 39 ++--- testapp/tests.py | 13 +- tox.ini | 61 ++++--- unittests/test_databases.py | 26 +-- 28 files changed, 523 insertions(+), 565 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d89e671 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + build: + if: github.repository == 'jazzband/django-nose' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install -U setuptools twine wheel + + - name: Build package + run: | + python setup.py --version + python setup.py sdist --format=gztar bdist_wheel + twine check dist/* + + - name: Upload packages to Jazzband + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + user: jazzband + password: ${{ secrets.JAZZBAND_RELEASE_KEY }} + repository_url: https://jazzband.co/projects/django-nose/upload diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..895dfa6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,78 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 5 + matrix: + python-version: ['3.5', '3.6', '3.7', '3.8'] + + services: + postgres: + image: postgres:10 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + mariadb: + image: mariadb:10.3 + env: + MYSQL_ROOT_PASSWORD: mysql + MYSQL_DATABASE: mysql + options: >- + --health-cmd "mysqladmin ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 3306:3306 + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: + ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} + restore-keys: | + ${{ matrix.python-version }}-v1- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox tox-gh-actions + + - name: Test with tox + run: tox + env: + COVERAGE: 1 + RUNTEST_ARGS: "-v --noinput" + + - name: Upload coverage + uses: codecov/codecov-action@v1 + with: + name: Python ${{ matrix.python-version }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6753f32..0000000 --- a/.travis.yml +++ /dev/null @@ -1,216 +0,0 @@ -sudo: false -language: python -env: - global: - - COVERAGE=1 RUNTEST_ARGS="-v --noinput" -matrix: - include: - - env: TOXENV=flake8 - python: '3.5' - - env: TOXENV=docs - python: '3.5' - - env: TOXENV=py35-django-22 - python: '3.5' - sudo: required - dist: xenial - - env: TOXENV=py36-django-22 - python: '3.6' - sudo: required - dist: xenial - - env: TOXENV=py37-django-22 - python: '3.7' - sudo: required - dist: xenial - - env: TOXENV=py35-django-21 - python: '3.5' - - env: TOXENV=py36-django-21 - python: '3.6' - - env: TOXENV=py37-django-21 - python: '3.7' - sudo: required - dist: xenial - - env: TOXENV=py35-django-20 - python: '3.5' - - env: TOXENV=py36-django-20 - python: '3.6' - - env: TOXENV=py27-django-111 - python: '2.7' - - env: TOXENV=py36-django-111 - python: '3.6' - - env: TOXENV=py27-django-110 - python: '2.7' - - env: TOXENV=py35-django-110 - python: '3.5' - - env: TOXENV=py27-django-19 - python: '2.7' - - env: TOXENV=py35-django-19 - python: '3.5' - - env: TOXENV=py27-django-18 - python: '2.7' - - env: TOXENV=py34-django-18 - python: '3.4' - - env: TOXENV=py35-django-22-postgres DATABASE_URL="postgres://postgres@localhost:5432/py35-django-22-postgres" - python: '3.5' - services: postgresql - sudo: required - dist: xenial - - env: TOXENV=py36-django-22-postgres DATABASE_URL="postgres://postgres@localhost:5432/py36-django-22-postgres" - python: '3.6' - services: postgresql - sudo: required - dist: xenial - - env: TOXENV=py37-django-22-postgres DATABASE_URL="postgres://postgres@localhost:5432/py37-django-22-postgres" - python: '3.7' - sudo: required - dist: xenial - services: postgresql - - env: TOXENV=py35-django-21-postgres DATABASE_URL="postgres://postgres@localhost:5432/py35-django-21-postgres" - python: '3.5' - services: postgresql - - env: TOXENV=py36-django-21-postgres DATABASE_URL="postgres://postgres@localhost:5432/py36-django-21-postgres" - python: '3.6' - services: postgresql - - env: TOXENV=py37-django-21-postgres DATABASE_URL="postgres://postgres@localhost:5432/py37-django-21-postgres" - python: '3.7' - sudo: required - dist: xenial - services: postgresql - - env: TOXENV=py35-django-20-postgres DATABASE_URL="postgres://postgres@localhost:5432/py35-django-20-postgres" - python: '3.5' - services: postgresql - - env: TOXENV=py36-django-20-postgres DATABASE_URL="postgres://postgres@localhost:5432/py36-django-20-postgres" - python: '3.6' - services: postgresql - - env: TOXENV=py37-django-20-postgres DATABASE_URL="postgres://postgres@localhost:5432/py37-django-20-postgres" - python: '3.7' - sudo: required - dist: xenial - services: postgresql - - env: TOXENV=py27-django-111-postgres DATABASE_URL="postgres://postgres@localhost:5432/py27-django-111-postgres" - python: '2.7' - services: postgresql - - env: TOXENV=py36-django-111-postgres DATABASE_URL="postgres://postgres@localhost:5432/py36-django-111-postgres" - python: '3.6' - services: postgresql - - env: TOXENV=py27-django-110-postgres DATABASE_URL="postgres://postgres@localhost:5432/py27-django-110-postgres" - python: '2.7' - services: postgresql - - env: TOXENV=py35-django-110-postgres DATABASE_URL="postgres://postgres@localhost:5432/py35-django-110-postgres" - python: '3.5' - services: postgresql - - env: TOXENV=py27-django-19-postgres DATABASE_URL="postgres://postgres@localhost:5432/py27-django-19-postgres" - python: '2.7' - services: postgresql - - env: TOXENV=py35-django-19-postgres DATABASE_URL="postgres://postgres@localhost:5432/py35-django-19-postgres" - python: '3.5' - services: postgresql - - env: TOXENV=py27-django-18-postgres DATABASE_URL="postgres://postgres@localhost:5432/py27-django-18-postgres" - python: '2.7' - services: postgresql - - env: TOXENV=py35-django-22-mysql DATABASE_URL="mysql://travis@localhost:3306/py35-django-22-mysql" - python: '3.5' - services: mysql - sudo: required - dist: xenial - - env: TOXENV=py36-django-22-mysql DATABASE_URL="mysql://travis@localhost:3306/py36-django-22-mysql" - python: '3.6' - services: mysql - sudo: required - dist: xenial - - env: TOXENV=py37-django-22-mysql DATABASE_URL="mysql://travis@localhost:3306/py37-django-22-mysql" - python: '3.7' - sudo: required - dist: xenial - services: mysql - - env: TOXENV=py35-django-21-mysql DATABASE_URL="mysql://travis@localhost:3306/py35-django-21-mysql" - python: '3.5' - services: mysql - - env: TOXENV=py36-django-21-mysql DATABASE_URL="mysql://travis@localhost:3306/py36-django-21-mysql" - python: '3.6' - services: mysql - - env: TOXENV=py37-django-21-mysql DATABASE_URL="mysql://travis@localhost:3306/py37-django-21-mysql" - python: '3.7' - sudo: required - dist: xenial - services: mysql - - env: TOXENV=py35-django-20-mysql DATABASE_URL="mysql://travis@localhost:3306/py35-django-20-mysql" - python: '3.5' - services: mysql - - env: TOXENV=py36-django-20-mysql DATABASE_URL="mysql://travis@localhost:3306/py36-django-20-mysql" - python: '3.6' - services: mysql - - env: TOXENV=py37-django-20-mysql DATABASE_URL="mysql://travis@localhost:3306/py37-django-20-mysql" - python: '3.7' - sudo: required - dist: xenial - services: mysql - - env: TOXENV=py27-django-111-mysql DATABASE_URL="mysql://travis@localhost:3306/py27-django-111-mysql" - python: '2.7' - services: mysql - - env: TOXENV=py36-django-111-mysql DATABASE_URL="mysql://travis@localhost:3306/py36-django-111-mysql" - python: '3.6' - services: mysql - - env: TOXENV=py27-django-110-mysql DATABASE_URL="mysql://travis@localhost:3306/py27-django-110-mysql" - python: '2.7' - services: mysql - - env: TOXENV=py35-django-110-mysql DATABASE_URL="mysql://travis@localhost:3306/py35-django-110-mysql" - python: '3.5' - services: mysql - - env: TOXENV=py27-django-19-mysql DATABASE_URL="mysql://travis@localhost:3306/py27-django-19-mysql" - python: '2.7' - services: mysql - - env: TOXENV=py35-django-19-mysql DATABASE_URL="mysql://travis@localhost:3306/py35-django-19-mysql" - python: '3.5' - services: mysql - - env: TOXENV=py27-django-18-mysql DATABASE_URL="mysql://travis@localhost:3306/py27-django-18-mysql" - python: '2.7' - services: mysql - - env: TOXENV=py36-django-master - python: '3.6' - sudo: required - dist: xenial - - env: TOXENV=py37-django-master - python: '3.7' - sudo: required - dist: xenial - - env: TOXENV=py38-django-master - python: '3.8' - sudo: required - dist: xenial - - env: TOXENV=py36-django-master-mysql DATABASE_URL="mysql://travis@localhost:3306/py36-django-master-mysql" - python: '3.6' - services: mysql - sudo: required - dist: xenial - - env: TOXENV=py37-django-master-postgres DATABASE_URL="postgres://postgres@localhost:5432/py37-django-master-postgres" - python: '3.7' - services: postgresql - sudo: required - dist: xenial - allow_failures: - - env: TOXENV=py36-django-master - - env: TOXENV=py37-django-master - - env: TOXENV=py38-django-master - - env: TOXENV=py36-django-master-mysql DATABASE_URL="mysql://travis@localhost:3306/py36-django-master-mysql" - - env: TOXENV=py37-django-master-postgres DATABASE_URL="postgres://postgres@localhost:5432/py37-django-master-postgres" -install: -- pip install tox coveralls -before_script: -- coverage erase -- bash -c "if [[ \"$DATABASE_URL\" == postgres* ]]; then psql -c 'create database - \"$TOXENV\";' -U postgres; fi" -- bash -c "if [[ \"$DATABASE_URL\" == mysql* ]]; then mysql -e 'create database IF - NOT EXISTS \`$TOXENV\`;'; fi" -script: tox -after_success: coveralls -deploy: - provider: pypi - user: jazzband - server: https://jazzband.co/projects/django-nose/upload - distributions: sdist bdist_wheel - password: - secure: tNMoNLBm4bNO7FxTVL30r1pG3mHxmoUs0KJhFiN5G7hioRnwmeYfnDDe8dlsxMUH7NNCZjhHNCJ1ll5rBTZhqTuXTXOrxzHigV18rRYiyPfzFsVsKfAhqmjROv0Jf1XgOAZ6VQM7A69eIMVE1gs05kJevDqJaRp2e7hPq7a2+HQ= - on: - tags: true - repo: jazzband/django-nose - condition: "$TOXENV = py37-django-22" diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5c95879..4151a22 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -125,7 +125,7 @@ Before you submit a pull request, check that it meets these guidelines: 5. Make liberal use of `git rebase` to ensure clean commits on top of master. 6. The pull request should pass QA tests and work for supported Python / Django combinations. Check - https://travis-ci.org/jazzband/django-nose/pull_requests + https://github.com/jazzband/django-nose/actions and make sure that the tests pass for all supported Python versions. Tips diff --git a/README.rst b/README.rst index 921c217..8bd5590 100644 --- a/README.rst +++ b/README.rst @@ -6,13 +6,13 @@ django-nose :alt: The PyPI package :target: https://pypi.python.org/pypi/django-nose -.. image:: https://img.shields.io/travis/jazzband/django-nose/master.svg - :alt: TravisCI Build Status - :target: https://travis-ci.org/jazzband/django-nose +.. image:: https://github.com/jazzband/django-nose/workflows/Test/badge.svg + :target: https://github.com/jazzband/django-nose/actions + :alt: GitHub Actions -.. image:: https://img.shields.io/coveralls/jazzband/django-nose/master.svg - :alt: Coveralls Test Coverage - :target: https://coveralls.io/r/jazzband/django-nose?branch=master +.. image:: https://codecov.io/gh/jazzband/django-nose/branch/master/graph/badge.svg + :alt: Coverage + :target: https://codecov.io/gh/jazzband/django-nose .. image:: https://jazzband.co/static/img/badge.svg :alt: Jazzband diff --git a/changelog.rst b/changelog.rst index 6442f5f..6d45766 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,6 +1,13 @@ Changelog --------- +unreleased +~~~~~~~~~~ + +* Dropped Python 2 support. +* Moved CI to + `GitHub Actions `_. + 1.4.7 (2020-08-19) ~~~~~~~~~~~~~~~~~~ * Document Django 2.2 support, no changes needed diff --git a/contribute.json b/contribute.json index 801deac..bb1e861 100644 --- a/contribute.json +++ b/contribute.json @@ -4,7 +4,7 @@ "repository": { "url": "https://github.com/jazzband/django-nose", "license": "BSD", - "tests": "https://travis-ci.org/jazzband/django-nose" + "tests": "https://github.com/jazzband/django-nose/actions" }, "participate": { "home": "https://github.com/jazzband/django-nose", diff --git a/django_nose/__init__.py b/django_nose/__init__.py index e84755a..7093beb 100644 --- a/django_nose/__init__.py +++ b/django_nose/__init__.py @@ -1,12 +1,16 @@ # coding: utf-8 """The django_nose module.""" -from __future__ import unicode_literals +from pkg_resources import get_distribution, DistributionNotFound from django_nose.runner import BasicNoseRunner, NoseTestSuiteRunner from django_nose.testcases import FastFixtureTestCase + assert BasicNoseRunner assert NoseTestSuiteRunner assert FastFixtureTestCase -VERSION = (1, 4, 6) -__version__ = '.'.join(map(str, VERSION)) +try: + __version__ = get_distribution("django-nose").version +except DistributionNotFound: + # package is not installed + pass diff --git a/django_nose/fixture_tables.py b/django_nose/fixture_tables.py index c727d8c..ae8ba42 100644 --- a/django_nose/fixture_tables.py +++ b/django_nose/fixture_tables.py @@ -23,8 +23,10 @@ def get_apps(): """Emulate get_apps in Django 1.9 and later.""" return [a.models_module for a in apps.get_app_configs()] + try: import bz2 + has_bz2 = True except ImportError: has_bz2 = False @@ -47,23 +49,20 @@ class SingleZipReader(zipfile.ZipFile): def __init__(self, *args, **kwargs): zipfile.ZipFile.__init__(self, *args, **kwargs) if settings.DEBUG: - assert len(self.namelist()) == 1, \ - "Zip-compressed fixtures must contain only one file." + assert ( + len(self.namelist()) == 1 + ), "Zip-compressed fixtures must contain only one file." def read(self): return zipfile.ZipFile.read(self, self.namelist()[0]) - compression_types = { - None: open, - 'gz': gzip.GzipFile, - 'zip': SingleZipReader - } + compression_types = {None: open, "gz": gzip.GzipFile, "zip": SingleZipReader} if has_bz2: - compression_types['bz2'] = bz2.BZ2File + compression_types["bz2"] = bz2.BZ2File app_module_paths = [] for app in get_apps(): - if hasattr(app, '__path__'): + if hasattr(app, "__path__"): # It's a 'models/' subpackage for path in app.__path__: app_module_paths.append(path) @@ -72,10 +71,10 @@ def read(self): app_module_paths.append(app.__file__) app_fixtures = [ - os.path.join(os.path.dirname(path), 'fixtures') - for path in app_module_paths] + os.path.join(os.path.dirname(path), "fixtures") for path in app_module_paths + ] for fixture_label in fixture_labels: - parts = fixture_label.split('.') + parts = fixture_label.split(".") if len(parts) > 1 and parts[-1] in compression_types: compression_formats = [parts[-1]] @@ -87,7 +86,7 @@ def read(self): fixture_name = parts[0] 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: @@ -101,7 +100,7 @@ def read(self): 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: # stdout.write("Checking %s for fixtures...\n" % @@ -110,11 +109,8 @@ def read(self): label_found = False for combo in product([using, None], formats, compression_formats): database, format, compression_format = combo - file_name = '.'.join( - p for p in [ - fixture_name, database, format, compression_format - ] - if p + file_name = ".".join( + p for p in [fixture_name, database, format, compression_format] if p ) # stdout.write("Trying %s for %s fixture '%s'...\n" % \ @@ -122,7 +118,7 @@ def read(self): full_path = os.path.join(fixture_dir, file_name) open_method = compression_types[compression_format] try: - fixture = open_method(full_path, 'r') + fixture = open_method(full_path, "r") if label_found: fixture.close() # stderr.write(style.ERROR("Multiple fixtures named @@ -137,7 +133,8 @@ def read(self): # % (format, fixture_name, humanize(fixture_dir))) try: objects = serializers.deserialize( - format, fixture, using=using) + format, fixture, using=using + ) for obj in objects: objects_in_fixture += 1 cls = obj.object.__class__ diff --git a/django_nose/management/commands/test.py b/django_nose/management/commands/test.py index 40390d7..649e540 100644 --- a/django_nose/management/commands/test.py +++ b/django_nose/management/commands/test.py @@ -4,8 +4,6 @@ This enables browsing all the nose options from the command line. """ -from __future__ import unicode_literals - from django.conf import settings from django.core.management.commands.test import Command from django.test.utils import get_runner @@ -13,7 +11,7 @@ TestRunner = get_runner(settings) -if hasattr(TestRunner, 'options'): +if hasattr(TestRunner, "options"): extra_options = TestRunner.options else: extra_options = [] @@ -22,4 +20,4 @@ class Command(Command): """Implement the ``test`` command.""" - option_list = getattr(Command, 'option_list', ()) + tuple(extra_options) + option_list = getattr(Command, "option_list", ()) + tuple(extra_options) diff --git a/django_nose/plugin.py b/django_nose/plugin.py index b164378..2cbd966 100644 --- a/django_nose/plugin.py +++ b/django_nose/plugin.py @@ -1,7 +1,5 @@ # coding: utf-8 """Included django-nose plugins.""" -from __future__ import unicode_literals - import sys from nose.plugins.base import Plugin @@ -38,7 +36,7 @@ class ResultPlugin(AlwaysOnPlugin): ``result`` after running the tests to get the TestResult object. """ - name = 'result' + name = "result" def finalize(self, result): """Finalize test run by capturing the result.""" @@ -52,7 +50,7 @@ class DjangoSetUpPlugin(AlwaysOnPlugin): initialization of the test runner. """ - name = 'django setup' + name = "django setup" score = 150 def __init__(self, runner): @@ -111,10 +109,10 @@ def add(self, test): if is_subclass_at_all(test.context, FastFixtureTestCase): # We bucket even FFTCs that don't have any fixtures, but it # shouldn't matter. - key = (frozenset(getattr(test.context, 'fixtures', [])), - getattr(test.context, - 'exempt_from_fixture_bundling', - False)) + key = ( + frozenset(getattr(test.context, "fixtures", [])), + getattr(test.context, "exempt_from_fixture_bundling", False), + ) self.buckets.setdefault(key, []).append(test) else: self.remainder.append(test) @@ -123,18 +121,20 @@ def add(self, test): class TestReorderer(AlwaysOnPlugin): """Reorder tests for various reasons.""" - name = 'django-nose-test-reorderer' + name = "django-nose-test-reorderer" def options(self, parser, env): """Add --with-fixture-bundling to options.""" super(TestReorderer, self).options(parser, env) # pointless - parser.add_option('--with-fixture-bundling', - action='store_true', - dest='with_fixture_bundling', - default=env.get('NOSE_WITH_FIXTURE_BUNDLING', False), - help='Load a unique set of fixtures only once, even ' - 'across test classes. ' - '[NOSE_WITH_FIXTURE_BUNDLING]') + parser.add_option( + "--with-fixture-bundling", + action="store_true", + dest="with_fixture_bundling", + default=env.get("NOSE_WITH_FIXTURE_BUNDLING", False), + help="Load a unique set of fixtures only once, even " + "across test classes. " + "[NOSE_WITH_FIXTURE_BUNDLING]", + ) def configure(self, options, conf): """Configure plugin, reading the with_fixture_bundling option.""" @@ -156,6 +156,7 @@ def _put_transaction_test_cases_last(self, test): you'd have to clean on entry to a test anyway." was once uttered on #django-dev. """ + def filthiness(test): """Return a score of how messy a test leaves the environment. @@ -183,9 +184,10 @@ def filthiness(test): """ test_class = test.context - if (is_subclass_at_all(test_class, TestCase) or - (is_subclass_at_all(test_class, TransactionTestCase) and - getattr(test_class, 'cleans_up_after_itself', False))): + if is_subclass_at_all(test_class, TestCase) or ( + is_subclass_at_all(test_class, TransactionTestCase) + and getattr(test_class, "cleans_up_after_itself", False) + ): return 1 return 2 @@ -208,6 +210,7 @@ def _bundle_fixtures(self, test): bits. We return those first, then any remaining tests in the order they were received. """ + def suite_sorted_by_fixtures(suite): """Flatten and sort a tree of Suites by fixture. diff --git a/django_nose/runner.py b/django_nose/runner.py index 98db33e..a36cded 100644 --- a/django_nose/runner.py +++ b/django_nose/runner.py @@ -8,8 +8,6 @@ in settings.py for arguments that you want always passed to nose. """ -from __future__ import print_function, unicode_literals - import os import sys from importlib import import_module @@ -29,45 +27,45 @@ from django_nose.utils import uses_mysql import nose.core -__all__ = ('BasicNoseRunner', 'NoseTestSuiteRunner') +__all__ = ("BasicNoseRunner", "NoseTestSuiteRunner") # This is a table of Django's "manage.py test" options which # correspond to nosetests options with a different name: -OPTION_TRANSLATION = {'--failfast': '-x', - '--nose-verbosity': '--verbosity'} +OPTION_TRANSLATION = {"--failfast": "-x", "--nose-verbosity": "--verbosity"} def translate_option(opt): - if '=' in opt: - long_opt, value = opt.split('=', 1) - return '%s=%s' % (translate_option(long_opt), value) + if "=" in opt: + long_opt, value = opt.split("=", 1) + return "%s=%s" % (translate_option(long_opt), value) return OPTION_TRANSLATION.get(opt, opt) def _get_plugins_from_settings(): - plugins = (list(getattr(settings, 'NOSE_PLUGINS', [])) + - ['django_nose.plugin.TestReorderer']) - for plug_path in plugins: + settings_plugins = list(getattr(settings, "NOSE_PLUGINS", [])) + for plug_path in settings_plugins + ["django_nose.plugin.TestReorderer"]: try: - dot = plug_path.rindex('.') + dot = plug_path.rindex(".") except ValueError: raise exceptions.ImproperlyConfigured( - "%s isn't a Nose plugin module" % plug_path) - p_mod, p_classname = plug_path[:dot], plug_path[dot + 1:] + "%s isn't a Nose plugin module" % plug_path + ) + p_mod, p_classname = plug_path[:dot], plug_path[dot + 1 :] try: mod = import_module(p_mod) except ImportError as e: raise exceptions.ImproperlyConfigured( - 'Error importing Nose plugin module %s: "%s"' % (p_mod, e)) + 'Error importing Nose plugin module %s: "%s"' % (p_mod, e) + ) try: p_class = getattr(mod, p_classname) except AttributeError: raise exceptions.ImproperlyConfigured( - 'Nose plugin module "%s" does not define a "%s"' % - (p_mod, p_classname)) + 'Nose plugin module "%s" does not define a "%s"' % (p_mod, p_classname) + ) yield p_class() @@ -83,16 +81,23 @@ class BaseRunner(DiscoverRunner): # Don't pass the following options to nosetests django_opts = [ - '--noinput', '--liveserver', '-p', '--pattern', '--testrunner', - '--settings', + "--noinput", + "--liveserver", + "-p", + "--pattern", + "--testrunner", + "--settings", # 1.8 arguments - '--keepdb', '--reverse', '--debug-sql', + "--keepdb", + "--reverse", + "--debug-sql", # 1.9 arguments - '--parallel', + "--parallel", # 1.10 arguments - '--tag', '--exclude-tag', + "--tag", + "--exclude-tag", # 1.11 arguments - '--debug-mode', + "--debug-mode", ] # @@ -100,32 +105,40 @@ class BaseRunner(DiscoverRunner): # # Option strings to remove from Django options if found _argparse_remove_options = ( - '-p', # Short arg for nose's --plugins, not Django's --patterns - '-d', # Short arg for nose's --detailed-errors, not Django's - # --debug-sql + "-p", # Short arg for nose's --plugins, not Django's --patterns + "-d", # Short arg for nose's --detailed-errors, not Django's + # --debug-sql ) # Convert nose optparse options to argparse options _argparse_type = { - 'int': int, - 'float': float, - 'complex': complex, - 'string': str, - 'choice': str, + "int": int, + "float": float, + "complex": complex, + "string": str, + "choice": str, } # If optparse has a None argument, omit from call to add_argument _argparse_omit_if_none = ( - 'action', 'nargs', 'const', 'default', 'type', 'choices', - 'required', 'help', 'metavar', 'dest') + "action", + "nargs", + "const", + "default", + "type", + "choices", + "required", + "help", + "metavar", + "dest", + ) # Always ignore these optparse arguments # Django will parse without calling the callback # nose will then reparse with the callback - _argparse_callback_options = ( - 'callback', 'callback_args', 'callback_kwargs') + _argparse_callback_options = ("callback", "callback_args", "callback_kwargs") # Keep track of nose options with nargs=1 - _has_nargs = set(['--verbosity']) + _has_nargs = set(["--verbosity"]) @classmethod def add_arguments(cls, parser): @@ -135,8 +148,7 @@ def add_arguments(cls, parser): # Read optparse options for nose and plugins cfg_files = nose.core.all_config_files() manager = nose.core.DefaultPluginManager() - config = nose.core.Config( - env=os.environ, files=cfg_files, plugins=manager) + config = nose.core.Config(env=os.environ, files=cfg_files, plugins=manager) config.plugins.addPlugins(list(_get_plugins_from_settings())) options = config.getParser()._get_all_options() @@ -146,8 +158,7 @@ def add_arguments(cls, parser): for override in cls._argparse_remove_options: if override in action.option_strings: # Emulate parser.conflict_handler='resolve' - parser._handle_conflict_resolve( - None, ((override, action),)) + parser._handle_conflict_resolve(None, ((override, action),)) django_options.update(action.option_strings) # Process nose optparse options @@ -160,8 +171,8 @@ def add_arguments(cls, parser): opt_short = None # Rename nose's --verbosity to --nose-verbosity - if opt_long == '--verbosity': - opt_long = '--nose-verbosity' + if opt_long == "--verbosity": + opt_long = "--nose-verbosity" # Skip any options also in Django options if opt_long in django_options: @@ -178,33 +189,33 @@ def add_arguments(cls, parser): value = getattr(option, attr) - if attr == 'default' and value == NO_DEFAULT: + if attr == "default" and value == NO_DEFAULT: continue # Rename options for nose's --verbosity - if opt_long == '--nose-verbosity': - if attr == 'dest': - value = 'nose_verbosity' - elif attr == 'metavar': - value = 'NOSE_VERBOSITY' + if opt_long == "--nose-verbosity": + if attr == "dest": + value = "nose_verbosity" + elif attr == "metavar": + value = "NOSE_VERBOSITY" # Omit arguments that are None, use default if attr in cls._argparse_omit_if_none and value is None: continue # Convert type from optparse string to argparse type - if attr == 'type': + if attr == "type": value = cls._argparse_type[value] # Convert action='callback' to action='store' - if attr == 'action' and value == 'callback': - action = 'store' + if attr == "action" and value == "callback": + action = "store" # Keep track of nargs=1 - if attr == 'nargs': + if attr == "nargs": assert value == 1, ( - 'argparse option nargs=%s is not supported' % - value) + "argparse option nargs=%s is not supported" % value + ) cls._has_nargs.add(opt_long) if opt_short: cls._has_nargs.add(opt_short) @@ -232,17 +243,14 @@ class BasicNoseRunner(BaseRunner): def run_suite(self, nose_argv): """Run the test suite.""" result_plugin = ResultPlugin() - plugins_to_add = [DjangoSetUpPlugin(self), - result_plugin, - TestReorderer()] + plugins_to_add = [DjangoSetUpPlugin(self), result_plugin, TestReorderer()] for plugin in _get_plugins_from_settings(): plugins_to_add.append(plugin) setup() - nose.core.TestProgram(argv=nose_argv, exit=False, - addplugins=plugins_to_add) + nose.core.TestProgram(argv=nose_argv, exit=False, addplugins=plugins_to_add) return result_plugin.result def run_tests(self, test_labels, extra_tests=None): @@ -273,16 +281,16 @@ def run_tests(self, test_labels, extra_tests=None): Returns the number of tests that failed. """ - nose_argv = (['nosetests'] + list(test_labels)) - if hasattr(settings, 'NOSE_ARGS'): + nose_argv = ["nosetests"] + list(test_labels) + if hasattr(settings, "NOSE_ARGS"): nose_argv.extend(settings.NOSE_ARGS) # Recreate the arguments in a nose-compatible format arglist = sys.argv[1:] - has_nargs = getattr(self, '_has_nargs', set(['--verbosity'])) + has_nargs = getattr(self, "_has_nargs", set(["--verbosity"])) while arglist: opt = arglist.pop(0) - if not opt.startswith('-'): + if not opt.startswith("-"): # Discard test labels continue if any(opt.startswith(d) for d in self.django_opts): @@ -298,12 +306,13 @@ def run_tests(self, test_labels, extra_tests=None): nose_argv.append(opt_value) # if --nose-verbosity was omitted, pass Django verbosity to nose - if ('--verbosity' not in nose_argv and - not any(opt.startswith('--verbosity=') for opt in nose_argv)): - nose_argv.append('--verbosity=%s' % str(self.verbosity)) + if "--verbosity" not in nose_argv and not any( + opt.startswith("--verbosity=") for opt in nose_argv + ): + nose_argv.append("--verbosity=%s" % str(self.verbosity)) if self.verbosity >= 1: - print(' '.join(nose_argv)) + print(" ".join(nose_argv)) result = self.run_suite(nose_argv) # suite_result expects the suite as the first argument. Fake it. @@ -319,23 +328,24 @@ def _foreign_key_ignoring_handle(self, *fixture_labels, **options): This allows loading circular references from fixtures, and is monkeypatched into place in setup_databases(). """ - using = options.get('database', DEFAULT_DB_ALIAS) + using = options.get("database", DEFAULT_DB_ALIAS) connection = connections[using] # MySQL stinks at loading circular references: if uses_mysql(connection): cursor = connection.cursor() - cursor.execute('SET foreign_key_checks = 0') + cursor.execute("SET foreign_key_checks = 0") _old_handle(self, *fixture_labels, **options) if uses_mysql(connection): cursor = connection.cursor() - cursor.execute('SET foreign_key_checks = 1') + cursor.execute("SET foreign_key_checks = 1") -def _skip_create_test_db(self, verbosity=1, autoclobber=False, serialize=True, - keepdb=True): +def _skip_create_test_db( + self, verbosity=1, autoclobber=False, serialize=True, keepdb=True +): """``create_test_db`` implementation that skips both creation and flushing. The idea is to re-use the perfectly good test DB already created by an @@ -347,27 +357,27 @@ def _skip_create_test_db(self, verbosity=1, autoclobber=False, serialize=True, # (https://code.djangoproject.com/ticket/12991) but removed in Django v1.5 # (https://code.djangoproject.com/ticket/17760). In Django v1.5 # supports_transactions is a cached property evaluated on access. - if callable(getattr(self.connection.features, 'confirm', None)): + if callable(getattr(self.connection.features, "confirm", None)): # Django v1.3-4 self.connection.features.confirm() elif hasattr(self, "_rollback_works"): # Django v1.2 and lower can_rollback = self._rollback_works() - self.connection.settings_dict['SUPPORTS_TRANSACTIONS'] = can_rollback + self.connection.settings_dict["SUPPORTS_TRANSACTIONS"] = can_rollback return self._get_test_db_name() def _reusing_db(): """Return whether the ``REUSE_DB`` flag was passed.""" - return os.getenv('REUSE_DB', 'false').lower() in ('true', '1') + return os.getenv("REUSE_DB", "false").lower() in ("true", "1") def _can_support_reuse_db(connection): """Return True if REUSE_DB is a sensible option for the backend.""" # Perhaps this is a SQLite in-memory DB. Those are created implicitly when # you try to connect to them, so our usual test doesn't work. - return not connection.creation._get_test_db_name() == ':memory:' + return not connection.creation._get_test_db_name() == ":memory:" def _should_create_database(connection): @@ -403,12 +413,13 @@ def _mysql_reset_sequences(style, connection): """Return a SQL statements needed to reset Django tables.""" tables = connection.introspection.django_table_names(only_existing=True) flush_statements = connection.ops.sql_flush( - style, tables, connection.introspection.sequence_list()) + style, tables, connection.introspection.sequence_list() + ) # connection.ops.sequence_reset_sql() is not implemented for MySQL, # and the base class just returns []. TODO: Implement it by pulling # the relevant bits out of sql_flush(). - return [s for s in flush_statements if s.startswith('ALTER')] + return [s for s in flush_statements if s.startswith("ALTER")] # Being overzealous and resetting the sequences on non-empty tables # like django_content_type seems to be fine in MySQL: adding a row # afterward does find the correct sequence number rather than @@ -428,8 +439,7 @@ class NoseTestSuiteRunner(BasicNoseRunner): def _get_models_for_connection(self, connection): """Return a list of models for a connection.""" tables = connection.introspection.get_table_list(connection.cursor()) - return [m for m in apps.get_models() if - m._meta.db_table in tables] + return [m for m in apps.get_models() if m._meta.db_table in tables] def setup_databases(self): """Set up databases. Skip DB creation if requested and possible.""" @@ -441,13 +451,13 @@ def setup_databases(self): # Mess with the DB name so other things operate on a test DB # rather than the real one. This is done in create_test_db when # we don't monkeypatch it away with _skip_create_test_db. - orig_db_name = connection.settings_dict['NAME'] - connection.settings_dict['NAME'] = test_db_name + orig_db_name = connection.settings_dict["NAME"] + connection.settings_dict["NAME"] = test_db_name if _should_create_database(connection): # We're not using _skip_create_test_db, so put the DB name # back: - connection.settings_dict['NAME'] = orig_db_name + connection.settings_dict["NAME"] = orig_db_name # Since we replaced the connection with the test DB, closing # the connection will avoid pooling issues with SQLAlchemy. The @@ -463,11 +473,11 @@ def setup_databases(self): style = no_style() if uses_mysql(connection): - reset_statements = _mysql_reset_sequences( - style, connection) + reset_statements = _mysql_reset_sequences(style, connection) else: reset_statements = connection.ops.sequence_reset_sql( - style, self._get_models_for_connection(connection)) + style, self._get_models_for_connection(connection) + ) if hasattr(transaction, "atomic"): with transaction.atomic(using=connection.alias): @@ -481,8 +491,7 @@ def setup_databases(self): # Each connection has its own creation object, so this affects # only a single connection: - creation.create_test_db = MethodType( - _skip_create_test_db, creation) + creation.create_test_db = MethodType(_skip_create_test_db, creation) Command.handle = _foreign_key_ignoring_handle @@ -493,6 +502,5 @@ def setup_databases(self): def teardown_databases(self, *args, **kwargs): """Leave those poor, reusable databases alone if REUSE_DB is true.""" if not _reusing_db(): - return super(NoseTestSuiteRunner, self).teardown_databases( - *args, **kwargs) + return super(NoseTestSuiteRunner, self).teardown_databases(*args, **kwargs) # else skip tearing down the DB so we can reuse it next time diff --git a/django_nose/testcases.py b/django_nose/testcases.py index 836a7d6..7369c05 100644 --- a/django_nose/testcases.py +++ b/django_nose/testcases.py @@ -1,7 +1,5 @@ # coding: utf-8 """TestCases that enable extra django-nose functionality.""" -from __future__ import unicode_literals - from django import test from django.conf import settings from django.core import cache, mail @@ -12,7 +10,7 @@ from django_nose.utils import uses_mysql -__all__ = ('FastFixtureTestCase', ) +__all__ = ("FastFixtureTestCase",) class FastFixtureTestCase(test.TransactionTestCase): @@ -42,8 +40,9 @@ class FastFixtureTestCase(test.TransactionTestCase): def setUpClass(cls): """Turn on manual commits. Load and commit the fixtures.""" if not test.testcases.connections_support_transactions(): - raise NotImplementedError('%s supports only DBs with transaction ' - 'capabilities.' % cls.__name__) + raise NotImplementedError( + "%s supports only DBs with transaction " "capabilities." % cls.__name__ + ) for db in cls._databases(): # These MUST be balanced with one leave_* each: transaction.enter_transaction_management(using=db) @@ -67,13 +66,16 @@ def tearDownClass(cls): def _fixture_setup(cls): """Load fixture data, and commit.""" for db in cls._databases(): - if (hasattr(cls, 'fixtures') and - getattr(cls, '_fb_should_setup_fixtures', True)): + if hasattr(cls, "fixtures") and getattr( + cls, "_fb_should_setup_fixtures", True + ): # Iff the fixture-bundling test runner tells us we're the first # suite having these fixtures, set them up: - call_command('loaddata', *cls.fixtures, **{'verbosity': 0, - 'commit': False, - 'database': db}) + call_command( + "loaddata", + *cls.fixtures, + **{"verbosity": 0, "commit": False, "database": db} + ) # No matter what, to preserve the effect of cursor start-up # statements... transaction.commit(using=db) @@ -81,8 +83,9 @@ def _fixture_setup(cls): @classmethod def _fixture_teardown(cls): """Empty (only) the tables we loaded fixtures into, then commit.""" - if hasattr(cls, 'fixtures') and \ - getattr(cls, '_fb_should_teardown_fixtures', True): + if hasattr(cls, "fixtures") and getattr( + cls, "_fb_should_teardown_fixtures", True + ): # If the fixture-bundling test runner advises us that the next test # suite is going to reuse these fixtures, don't tear them down. for db in cls._databases(): @@ -100,15 +103,15 @@ def _fixture_teardown(cls): # were loading additional Sites with a fixture, and then # the Django-provided example.com site was evaporating. if uses_mysql(connection): - cursor.execute('SET FOREIGN_KEY_CHECKS=0') + cursor.execute("SET FOREIGN_KEY_CHECKS=0") for table in tables: # Truncate implicitly commits. - cursor.execute('TRUNCATE `%s`' % table) + cursor.execute("TRUNCATE `%s`" % table) # TODO: necessary? - cursor.execute('SET FOREIGN_KEY_CHECKS=1') + cursor.execute("SET FOREIGN_KEY_CHECKS=1") else: for table in tables: - cursor.execute('DELETE FROM %s' % table) + cursor.execute("DELETE FROM %s" % table) transaction.commit(using=db) # cursor.close() # Should be unnecessary, since we committed @@ -132,6 +135,7 @@ def _pre_setup(self): # Clear site cache in case somebody's mutated Site objects and then # cached the mutated stuff: from django.contrib.sites.models import Site + Site.objects.clear_cache() def _post_teardown(self): @@ -157,7 +161,7 @@ def _post_teardown(self): @classmethod def _databases(cls): - if getattr(cls, 'multi_db', False): + if getattr(cls, "multi_db", False): return connections else: return [DEFAULT_DB_ALIAS] diff --git a/django_nose/tools.py b/django_nose/tools.py index c341a1d..2d0bf61 100644 --- a/django_nose/tools.py +++ b/django_nose/tools.py @@ -8,9 +8,10 @@ def _get_nose_vars(): """Collect assert_*, ok_, and eq_ from nose.tools.""" from nose import tools + new_names = {} for t in dir(tools): - if t.startswith('assert_') or t in ('ok_', 'eq_'): + if t.startswith("assert_") or t in ("ok_", "eq_"): new_names[t] = getattr(tools, t) return new_names @@ -23,15 +24,16 @@ def _get_django_vars(): """Collect assert_* methods from Django's TransactionTestCase.""" import re from django.test.testcases import TransactionTestCase - camelcase = re.compile('([a-z][A-Z]|[A-Z][a-z])') + + camelcase = re.compile("([a-z][A-Z]|[A-Z][a-z])") def insert_underscore(m): - """Insert an appropriate underscore into the name.""" - a, b = m.group(0) - if b.islower(): - return '_{}{}'.format(a, b) - else: - return '{}_{}'.format(a, b) + """Insert an appropriate underscore into the name.""" + a, b = m.group(0) + if b.islower(): + return "_{}{}".format(a, b) + else: + return "{}_{}".format(a, b) def pep8(name): """Replace camelcase name with PEP8 equivalent.""" @@ -43,11 +45,13 @@ class Dummy(TransactionTestCase): def nop(): """Do nothing, dummy test to get an initialized test case.""" pass - dummy_test = Dummy('nop') + + dummy_test = Dummy("nop") new_names = {} - for assert_name in [at for at in dir(dummy_test) - if at.startswith('assert') and '_' not in at]: + for assert_name in [ + at for at in dir(dummy_test) if at.startswith("assert") and "_" not in at + ]: pepd = pep8(assert_name) new_names[pepd] = getattr(dummy_test, assert_name) return new_names @@ -61,17 +65,19 @@ def nop(): # Additional assertions # -def assert_code(response, status_code, msg_prefix=''): + +def assert_code(response, status_code, msg_prefix=""): """Assert the response was returned with the given status code.""" if msg_prefix: - msg_prefix = '%s: ' % msg_prefix + msg_prefix = "%s: " % msg_prefix - assert response.status_code == status_code, \ - 'Response code was %d (expected %d)' % ( - response.status_code, status_code) + assert response.status_code == status_code, "Response code was %d (expected %d)" % ( + response.status_code, + status_code, + ) -def assert_ok(response, msg_prefix=''): +def assert_ok(response, msg_prefix=""): """Assert the response was returned with status 200 (OK).""" return assert_code(response, 200, msg_prefix=msg_prefix) @@ -85,7 +91,7 @@ def assert_mail_count(count, msg=None): from django.core import mail if msg is None: - msg = ', '.join([e.subject for e in mail.outbox]) - msg = '%d != %d %s' % (len(mail.outbox), count, msg) + msg = ", ".join([e.subject for e in mail.outbox]) + msg = "%d != %d %s" % (len(mail.outbox), count, msg) # assert_equals is dynamicaly added above. F821 is undefined name error assert_equals(len(mail.outbox), count, msg) # noqa: F821 diff --git a/django_nose/utils.py b/django_nose/utils.py index 139ae3a..0a38159 100644 --- a/django_nose/utils.py +++ b/django_nose/utils.py @@ -1,6 +1,5 @@ # coding: utf-8 """django-nose utility methods.""" -from __future__ import unicode_literals def process_tests(suite, process): @@ -25,8 +24,9 @@ def process_tests(suite, process): :arg process: The thing to call once we get to a leaf or a test with setup or teardown """ - if (not hasattr(suite, '_tests') or - (hasattr(suite, 'hasFixtures') and suite.hasFixtures())): + if not hasattr(suite, "_tests") or ( + hasattr(suite, "hasFixtures") and suite.hasFixtures() + ): # We hit a Test or something with setup, so do the thing. (Note that # "fixtures" here means setup or teardown routines, not Django # fixtures.) @@ -50,4 +50,4 @@ def is_subclass_at_all(cls, class_info): def uses_mysql(connection): """Return whether the connection represents a MySQL DB.""" - return 'mysql' in connection.settings_dict['ENGINE'] + return "mysql" in connection.settings_dict["ENGINE"] diff --git a/docs/conf.py b/docs/conf.py index 04b49df..ee1d4c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,6 @@ All configuration values have a default; values that are commented out serve to show the default. """ -from __future__ import unicode_literals from datetime import date import sys import os @@ -22,7 +21,7 @@ sys.path.append(parent) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") -from django_nose import __version__ # flake8: noqa +from django_nose import __version__ # noqa # -- General configuration ------------------------------------------------ @@ -32,24 +31,23 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'django-nose' -copyright = ( - '2010-%d, Jeff Balogh and the django-nose team.' % date.today().year) +project = "django-nose" +copyright = "2010-%d, Jeff Balogh and the django-nose team." % date.today().year # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -72,7 +70,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. @@ -90,7 +88,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -103,7 +101,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -132,7 +130,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -181,7 +179,7 @@ # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-nose-doc' +htmlhelp_basename = "django-nose-doc" # -- Options for LaTeX output --------------------------------------------- @@ -189,10 +187,8 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', } @@ -201,8 +197,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'django-nose.tex', 'django-nose Documentation', - 'Jeff Balogh and the django-nose team', 'manual'), + ( + "index", + "django-nose.tex", + "django-nose Documentation", + "Jeff Balogh and the django-nose team", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -231,8 +232,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-nose', 'django-nose Documentation', - ['Jeff Balogh', 'the django-nose team'], 1) + ( + "index", + "django-nose", + "django-nose Documentation", + ["Jeff Balogh", "the django-nose team"], + 1, + ) ] # If true, show URL addresses after external links. @@ -245,9 +251,14 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-nose', 'django-nose Documentation', - 'Jeff Balogh and the django-nose team', 'django-nose', - 'Makes your Django tests simple and snappy') + ( + "index", + "django-nose", + "django-nose Documentation", + "Jeff Balogh and the django-nose team", + "django-nose", + "Makes your Django tests simple and snappy", + ) ] # Documents to append as an appendix to all manuals. diff --git a/requirements.txt b/requirements.txt index 96f49d8..d732e41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ Django>=2.2,<3.0 -e . # Load database config from environment -dj-database-url==0.4.2 +django-environ # Packaging wheel==0.29.0 diff --git a/runtests.sh b/runtests.sh index 82b6356..768d09b 100755 --- a/runtests.sh +++ b/runtests.sh @@ -36,6 +36,7 @@ then fi export PYTHONPATH=. +export DATABASE_URL=${DATABASE_URL:-"sqlite:////tmp/test.db"} HAS_HOTSHOT=$(python -c "\ try: diff --git a/setup.cfg b/setup.cfg index 6fe40b5..67be1fe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,5 @@ [check-manifest] ignore = - .travis.yml tox.ini [coverage:run] @@ -12,7 +11,6 @@ omit = testapp/migrations/*.py [flake8] -exclude = .tox/*,build/* - -[wheel] -universal = 1 +exclude = .tox/*,build/*,.eggs/* +max-line-length = 88 +extend-ignore = E203, W503 diff --git a/setup.py b/setup.py index 2a14a45..c640f65 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """django-nose packaging.""" -from __future__ import unicode_literals import os from codecs import open from setuptools import setup, find_packages @@ -11,20 +10,21 @@ def get_long_description(title): """Create the long_description from other files.""" ROOT = os.path.abspath(os.path.dirname(__file__)) - readme = open(os.path.join(ROOT, 'README.rst'), 'r', 'utf8').read() + readme = open(os.path.join(ROOT, "README.rst"), "r", "utf8").read() body_tag = ".. Omit badges from docs" readme_body_start = readme.index(body_tag) assert readme_body_start - readme_body = readme[readme_body_start + len(body_tag):] + readme_body = readme[readme_body_start + len(body_tag) :] - changelog = open(os.path.join(ROOT, 'changelog.rst'), 'r', 'utf8').read() + changelog = open(os.path.join(ROOT, "changelog.rst"), "r", "utf8").read() old_tag = ".. Omit older changes from package" changelog_body_end = changelog.index(old_tag) assert changelog_body_end changelog_body = changelog[:changelog_body_end] - bars = '=' * len(title) - long_description = """ + bars = "=" * len(title) + long_description = ( + """ %(bars)s %(title)s %(bars)s @@ -33,26 +33,29 @@ def get_long_description(title): %(changelog_body)s _(Older changes can be found in the full documentation)._ -""" % locals() +""" + % locals() + ) return long_description setup( - name='django-nose', - version='1.4.7', - description='Makes your Django tests simple and snappy', - long_description=get_long_description('django-nose'), - author='Jeff Balogh', - author_email='me@jeffbalogh.org', - maintainer='John Whitlock', - maintainer_email='jwhitlock@mozilla.com', - url='http://github.com/jazzband/django-nose', - license='BSD', - packages=find_packages(exclude=['testapp', 'testapp/*']), + name="django-nose", + use_scm_version={"version_scheme": "post-release"}, + setup_requires=["setuptools_scm"], + description="Makes your Django tests simple and snappy", + long_description=get_long_description("django-nose"), + author="Jeff Balogh", + author_email="me@jeffbalogh.org", + maintainer="John Whitlock", + maintainer_email="jwhitlock@mozilla.com", + url="http://github.com/jazzband/django-nose", + license="BSD", + packages=find_packages(exclude=["testapp", "testapp/*"]), include_package_data=True, zip_safe=False, - install_requires=['nose>=1.2.1'], - test_suite='testapp.runtests.runtests', + install_requires=["nose>=1.2.1"], + test_suite="testapp.runtests.runtests", # This blows up tox runs that install django-nose into a virtualenv, # because it causes Nose to import django_nose.runner before the Django # settings are initialized, leading to a mess of errors. There's no reason @@ -63,30 +66,28 @@ def get_long_description(title): # [nose.plugins.0.10] # fixture_bundler = django_nose.fixture_bundling:FixtureBundlingPlugin # """, - keywords='django nose django-nose', + keywords="django nose django-nose", classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Framework :: Django :: 1.8', - 'Framework :: Django :: 1.9', - 'Framework :: Django :: 1.10', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', - 'Framework :: Django :: 2.2', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Topic :: Software Development :: Testing' - ] + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 1.8", + "Framework :: Django :: 1.9", + "Framework :: Django :: 1.10", + "Framework :: Django :: 1.11", + "Framework :: Django :: 2.0", + "Framework :: Django :: 2.1", + "Framework :: Django :: 2.2", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Software Development :: Testing", + ], ) diff --git a/testapp/migrations/0001_initial.py b/testapp/migrations/0001_initial.py index fafc22f..61edcac 100644 --- a/testapp/migrations/0001_initial.py +++ b/testapp/migrations/0001_initial.py @@ -1,42 +1,53 @@ # -*- coding: utf-8 -*- # flake8: noqa -from __future__ import unicode_literals - from django.db import models, migrations class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Choice', + name="Choice", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('choice_text', models.CharField(max_length=200)), - ('votes', models.IntegerField(default=0)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("choice_text", models.CharField(max_length=200)), + ("votes", models.IntegerField(default=0)), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( - name='Question', + name="Question", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('question_text', models.CharField(max_length=200)), - ('pub_date', models.DateTimeField(verbose_name=b'date published')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("question_text", models.CharField(max_length=200)), + ("pub_date", models.DateTimeField(verbose_name=b"date published")), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.AddField( - model_name='choice', - name='question', - field=models.ForeignKey(to='testapp.Question', on_delete=models.CASCADE), + model_name="choice", + name="question", + field=models.ForeignKey(to="testapp.Question", on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/testapp/models.py b/testapp/models.py index 9f471bf..1f28050 100644 --- a/testapp/models.py +++ b/testapp/models.py @@ -11,7 +11,7 @@ class Question(models.Model): """A poll question.""" question_text = models.CharField(max_length=200) - pub_date = models.DateTimeField('date published') + pub_date = models.DateTimeField("date published") def __str__(self): """Return string representation.""" diff --git a/testapp/plugin_t/test_with_plugins.py b/testapp/plugin_t/test_with_plugins.py index 61a9572..ea8cb6c 100644 --- a/testapp/plugin_t/test_with_plugins.py +++ b/testapp/plugin_t/test_with_plugins.py @@ -5,4 +5,5 @@ def test_one(): """Test that the test plugin was initialized.""" from testapp import plugins + eq_(plugins.plugin_began, True) diff --git a/testapp/runtests.py b/testapp/runtests.py index b1ac91e..b2c8131 100755 --- a/testapp/runtests.py +++ b/testapp/runtests.py @@ -6,8 +6,8 @@ if not settings.configured: settings.configure( - DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3'}}, - INSTALLED_APPS=['django_nose'], + DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3"}}, + INSTALLED_APPS=["django_nose"], MIDDLEWARE_CLASSES=[], ) @@ -15,10 +15,11 @@ def runtests(*test_labels): """Run the selected tests, or all tests if none selected.""" from django_nose import NoseTestSuiteRunner + runner = NoseTestSuiteRunner(verbosity=1, interactive=True) failures = runner.run_tests(test_labels) sys.exit(failures) -if __name__ == '__main__': +if __name__ == "__main__": runtests(*sys.argv[1:]) diff --git a/testapp/settings.py b/testapp/settings.py index a803307..83dcc8e 100644 --- a/testapp/settings.py +++ b/testapp/settings.py @@ -3,45 +3,32 @@ Configuration is overriden by environment variables: -DATABASE_URL - See https://github.com/kennethreitz/dj-database-url +DATABASE_URL - See https://github.com/joke2k/django-environ USE_SOUTH - Set to 1 to include South in INSTALLED_APPS TEST_RUNNER - Dotted path of test runner to use (can also use --test-runner) NOSE_PLUGINS - Comma-separated list of plugins to add """ -from __future__ import print_function -from os import environ, path +from os import path +import environ -import dj_database_url +env = environ.Env( + # set casting, default value + DEBUG=(bool, False) +) BASE_DIR = path.dirname(path.dirname(__file__)) - -def rel_path(*subpaths): - """Construct the full path given a relative path.""" - return path.join(BASE_DIR, *subpaths) - - -DATABASES = { - 'default': - dj_database_url.config( - default='sqlite:///' + rel_path('testapp.sqlite3')) -} +DATABASES = {"default": env.db("DATABASE_URL", default="sqlite:////tmp/test.sqlite")} MIDDLEWARE_CLASSES = () INSTALLED_APPS = [ - 'django_nose', - 'testapp', + "django_nose", + "testapp", ] -raw_test_runner = environ.get('TEST_RUNNER') -if raw_test_runner: - TEST_RUNNER = raw_test_runner -else: - TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' +TEST_RUNNER = env("TEST_RUNNER", default="django_nose.NoseTestSuiteRunner") -raw_plugins = environ.get('NOSE_PLUGINS') -if raw_plugins: - NOSE_PLUGINS = raw_plugins.split(',') +NOSE_PLUGINS = env.list("NOSE_PLUGINS", default=[]) -SECRET_KEY = 'ssshhhh' +SECRET_KEY = "ssshhhh" diff --git a/testapp/tests.py b/testapp/tests.py index f948099..97c798f 100644 --- a/testapp/tests.py +++ b/testapp/tests.py @@ -16,8 +16,8 @@ def test_question_str(self): def test_choice_str(self): """Test Choice.__str__ method.""" - choice = Choice(choice_text='My name is Sir Lancelot of Camelot.') - self.assertEqual('My name is Sir Lancelot of Camelot.', str(choice)) + choice = Choice(choice_text="My name is Sir Lancelot of Camelot.") + self.assertEqual("My name is Sir Lancelot of Camelot.", str(choice)) class UsesDatabaseTestCase(TestCase): @@ -26,9 +26,9 @@ class UsesDatabaseTestCase(TestCase): def test_question(self): """Test that votes is initialized to 0.""" question = Question.objects.create( - question_text="What is your quest?", pub_date=datetime(1975, 4, 9)) - Choice.objects.create( - question=question, choice_text="To seek the Holy Grail.") + question_text="What is your quest?", pub_date=datetime(1975, 4, 9) + ) + Choice.objects.create(question=question, choice_text="To seek the Holy Grail.") self.assertTrue(question.choice_set.exists()) the_choice = question.choice_set.get() self.assertEqual(0, the_choice.votes) @@ -42,8 +42,7 @@ class UsesFixtureTestCase(TransactionTestCase): def test_fixture_loaded(self): """Test that fixture was loaded.""" question = Question.objects.get() - self.assertEqual( - 'What is your favorite color?', question.question_text) + self.assertEqual("What is your favorite color?", question.question_text) self.assertEqual(datetime(1975, 4, 9), question.pub_date) choice = question.choice_set.get() self.assertEqual("Blue.", choice.choice_text) diff --git a/tox.ini b/tox.ini index ffd4174..cec7815 100644 --- a/tox.ini +++ b/tox.ini @@ -1,46 +1,65 @@ [tox] envlist = - py{27,34,35}-django-{18,19,110}{,-postgres,-mysql} - py{27,34,35,36}-django-111{,-postgres,-mysql} - py{34,35,36,37}-django-20{,-postgres,-mysql} - py{35,36,37}-django-{21,22}{,-postgres,-mysql} - py{36,37,38}-django-master{,-postgres,-mysql} + py35-dj{18,19,110}{,-postgres,-mysql} + py{35,36}-dj111{,-postgres,-mysql} + py{35,36,37}-dj20{,-postgres,-mysql} + py{35,36,37}-dj{21,22}{,-postgres,-mysql} + py{36,37,38}-djmaster{,-postgres,-mysql} flake8 docs skip_missing_interpreters = True +[gh-actions] +python = + 3.5: py35 + 3.6: py36 + 3.7: py37 + 3.8: py38, flake8, docs + [testenv] -passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH COVERAGE RUNTEST_ARGS DATABASE_URL +passenv= + CI + COVERAGE + RUNTEST_ARGS + GITHUB_* commands = ./runtests.sh {env:RUNTEST_ARGS:} - coverage combine + - coverage xml deps = - coveralls - dj-database-url - django-18: Django>=1.8,<1.9 - django-19: Django>=1.9,<1.10 - django-110: Django>=1.10,<1.11 - django-111: Django>=1.11,<2.0 - django-20: Django>=2.0,<2.1 - django-21: Django>=2.1,<2.2 - django-22: Django>=2.2,<3.0 - django-master: https://github.com/django/django/archive/master.tar.gz + coverage + django-environ + dj18: Django>=1.8,<1.9 + dj19: Django>=1.9,<1.10 + dj110: Django>=1.10,<1.11 + dj111: Django>=1.11,<2.0 + dj20: Django>=2.0,<2.1 + dj21: Django>=2.1,<2.2 + dj22: Django>=2.2,<3.0 + djmaster: https://github.com/django/django/archive/master.tar.gz postgres: psycopg2 mysql: mysqlclient +setenv = + DATABASE_URL = sqlite:////tmp/test.db + postgres: DATABASE_URL = postgres://postgres:postgres@localhost:5432/postgres + mysql: DATABASE_URL = mysql://root:mysql@127.0.0.1:3306/mysql + +[testenv:py{35,36,37,38}-djmaster{,-postgres,-mysql}] +ignore_errors = True [testenv:flake8] deps = Django - pep257==0.7.0 - pep8==1.6.2 - flake8==2.5.0 - flake8-docstrings==0.2.1 + pep257 + pep8 + flake8 + flake8-docstrings commands = flake8 [testenv:docs] changedir = docs deps = Sphinx - dj-database-url + django-environ Django commands = sphinx-build -b html -d {envtmpdir}/doctrees . {envtmpdir}/html diff --git a/unittests/test_databases.py b/unittests/test_databases.py index a3f5268..488fadb 100644 --- a/unittests/test_databases.py +++ b/unittests/test_databases.py @@ -4,7 +4,7 @@ try: from django.db.models.loading import cache as apps -except: +except ImportError: from django.apps import apps from nose.plugins.attrib import attr @@ -14,7 +14,7 @@ class GetModelsForConnectionTests(TestCase): """Test runner._get_models_for_connection.""" - tables = ['test_table%d' % i for i in range(5)] + tables = ["test_table%d" % i for i in range(5)] def _connection_mock(self, tables): class FakeIntrospection(object): @@ -31,7 +31,7 @@ def cursor(self): def _model_mock(self, db_table): class FakeModel(object): - _meta = type('meta', (object,), {'db_table': db_table})() + _meta = type("meta", (object,), {"db_table": db_table})() return FakeModel() @@ -52,16 +52,14 @@ def setUp(self): def test_no_models(self): """For a DB with no tables, return nothing.""" connection = self._connection_mock([]) - with self._cache_mock(['table1', 'table2']): - self.assertEqual( - self.runner._get_models_for_connection(connection), []) + with self._cache_mock(["table1", "table2"]): + self.assertEqual(self.runner._get_models_for_connection(connection), []) def test_wrong_models(self): """If no tables exists for models, return nothing.""" connection = self._connection_mock(self.tables) - with self._cache_mock(['table1', 'table2']): - self.assertEqual( - self.runner._get_models_for_connection(connection), []) + with self._cache_mock(["table1", "table2"]): + self.assertEqual(self.runner._get_models_for_connection(connection), []) @attr("special") def test_some_models(self): @@ -69,8 +67,9 @@ def test_some_models(self): connection = self._connection_mock(self.tables) with self._cache_mock(self.tables[1:3]): result_tables = [ - m._meta.db_table for m in - self.runner._get_models_for_connection(connection)] + m._meta.db_table + for m in self.runner._get_models_for_connection(connection) + ] self.assertEqual(result_tables, self.tables[1:3]) def test_all_models(self): @@ -78,6 +77,7 @@ def test_all_models(self): connection = self._connection_mock(self.tables) with self._cache_mock(self.tables): result_tables = [ - m._meta.db_table for m in - self.runner._get_models_for_connection(connection)] + m._meta.db_table + for m in self.runner._get_models_for_connection(connection) + ] self.assertEqual(result_tables, self.tables)