diff --git a/.editorconfig b/.editorconfig index 87fb28e32..d4649a5fa 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,9 @@ charset = utf-8 [Makefile] indent_style = tab +[*.{yaml,yml}] +indent_size = 2 + # We don't want to apply our defaults to third-party code or minified bundles: [**/{external,vendor}/**,**.min.{js,css}] indent_style = ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..4b5e1c762 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# Keep GitHub Actions up to date with GitHub's Dependabot... +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + groups: + github-actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request + schedule: + interval: weekly diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b8a15d08b..91fea6827 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,12 +2,12 @@ name: "CodeQL" on: push: - branches: [master, ] + branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - - cron: '0 6 * * 5' + - cron: "0 6 * * 5" jobs: analyze: @@ -15,14 +15,14 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: python + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 532eaea9a..edbe9af1a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,12 +6,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install dependencies - run: pip install sphinx - - name: Build docs - run: cd docs && make html + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install dependencies + run: pip install sphinx + - name: Build docs + run: cd docs && make html diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml deleted file mode 100644 index 6889aa5a4..000000000 --- a/.github/workflows/flake8.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: flake8 - -on: [pull_request, push] - -jobs: - check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install tools - run: pip install flake8 flake8-assertive flake8-bugbear flake8-builtins flake8-comprehensions flake8-logging-format - - name: Run flake8 - run: flake8 example_project haystack diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 13ae34cee..000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Publish - -on: - release: - types: [published] - -jobs: - publish: - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install dependencies - run: python -m pip install --upgrade pip setuptools twine wheel - - name: Build package - run: python setup.py sdist bdist_wheel - - name: Publish to PyPI - run: twine upload --non-interactive dist/* diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml new file mode 100644 index 000000000..7a158c5be --- /dev/null +++ b/.github/workflows/pypi-release.yml @@ -0,0 +1,38 @@ +name: "PyPI releases" + +on: release + +jobs: + build_sdist: + name: Build Python source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build sdist + run: pipx run build --sdist + + - uses: actions/upload-artifact@v4 + with: + path: dist/*.tar.gz + + pypi-publish: + name: Upload release to PyPI + if: github.event_name == 'release' && github.event.action == 'published' + needs: + - build_sdist + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/django-haystack + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + # unpacks default artifact into dist/ + # if `name: artifact` is omitted, the action will create extra parent dir + name: artifact + path: dist + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a7d67d3d3..257b6a42b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,29 +1,37 @@ name: Test -on: [pull_request, push] +on: + push: + branches: [master] + pull_request: + branches: [master] jobs: - test: + ruff: # https://docs.astral.sh/ruff + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pip install --user ruff + - run: ruff --output-format=github + test: runs-on: ubuntu-latest + needs: ruff # Do not run the tests if linting fails. strategy: - matrix: - django-version: [2.2, 3.1, 3.2] - python-version: [3.6, 3.7, 3.8, 3.9] - elastic-version: [1.7, 2.4, 5.5, '7.13.1'] - include: - - django-version: '4.0' - python-version: 3.8 - elastic-version: 5.5 - - django-version: '4.0' - python-version: 3.8 - elastic-version: '7.13.1' - - django-version: '4.0' - python-version: 3.9 - elastic-version: 5.5 - - django-version: '4.0' - python-version: 3.9 - elastic-version: '7.13.1' + fail-fast: false + matrix: # https://docs.djangoproject.com/en/stable/faq/install/#what-python-version-can-i-use-with-django + django-version: ["3.2", "4.2", "5.0"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + elastic-version: ["7.17.9"] + exclude: + - django-version: "3.2" + python-version: "3.11" + - django-version: "3.2" + python-version: "3.12" + - django-version: "5.0" + python-version: "3.8" + - django-version: "5.0" + python-version: "3.9" services: elastic: image: elasticsearch:${{ matrix.elastic-version }} @@ -39,20 +47,24 @@ jobs: solr: image: solr:6 ports: - - 9001:9001 + - 9001:8983 steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install system dependencies - run: sudo apt install --no-install-recommends -y gdal-bin - - name: Install dependencies - run: | - python -m pip install --upgrade pip setuptools wheel - pip install coverage requests - pip install django==${{ matrix.django-version }} elasticsearch==${{ matrix.elastic-version }} - python setup.py clean build install - - name: Run test - run: coverage run setup.py test + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install system dependencies + run: sudo apt install --no-install-recommends -y gdal-bin + - name: Setup solr test server in Docker + run: bash test_haystack/solr_tests/server/setup-solr-test-server-in-docker.sh + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + pip install coverage requests tox tox-gh-actions + pip install django==${{ matrix.django-version }} elasticsearch==${{ matrix.elastic-version }} + pip install --editable . + - name: Run test + run: tox -v + env: + DJANGO: ${{ matrix.django-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 026e9ec74..488a6b6a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,33 +1,54 @@ exclude: ".*/vendor/.*" repos: - - repo: https://github.com/PyCQA/isort - rev: 5.10.1 - hooks: - - id: isort - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 - hooks: - - id: check-added-large-files - args: ["--maxkb=128"] - - id: check-ast - - id: check-byte-order-marker - - id: check-case-conflict - - id: check-docstring-first - - id: check-executables-have-shebangs - - id: check-json - - id: check-merge-conflict - - id: check-symlinks - - id: check-xml - - id: check-yaml - - id: debug-statements - - id: detect-private-key - - id: end-of-file-fixer - - id: mixed-line-ending - args: ["--fix=lf"] - - id: pretty-format-json - args: ["--autofix", "--no-sort-keys", "--indent=4"] - - id: trailing-whitespace + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.18.0 + hooks: + - id: django-upgrade + args: [--target-version, "5.0"] # Replace with Django version + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.7 + hooks: + - id: ruff + # args: [ --fix, --exit-non-zero-on-fix ] + + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-added-large-files + args: ["--maxkb=128"] + - id: check-ast + - id: check-byte-order-marker + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-toml + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: end-of-file-fixer + - id: mixed-line-ending + args: ["--fix=lf"] + - id: pretty-format-json + args: ["--autofix", "--no-sort-keys", "--indent=4"] + - id: trailing-whitespace + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + types_or: [json, toml, xml, yaml] diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..134784f59 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,12 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py diff --git a/README.rst b/README.rst index 22afa29b1..f8d07d548 100644 --- a/README.rst +++ b/README.rst @@ -48,9 +48,8 @@ Documentation ============= * Development version: http://docs.haystacksearch.org/ -* v2.8.X: https://django-haystack.readthedocs.io/en/v2.8.1/ -* v2.7.X: https://django-haystack.readthedocs.io/en/v2.7.0/ -* v2.6.X: https://django-haystack.readthedocs.io/en/v2.6.0/ +* v3.3.0: https://django-haystack.readthedocs.io/en/v3.3.0/ +* v2.8.1: https://django-haystack.readthedocs.io/en/v2.8.1/ See the `changelog `_ @@ -59,8 +58,8 @@ Requirements Haystack has a relatively easily-met set of requirements. -* Python 3.6+ -* A supported version of Django: https://www.djangoproject.com/download/#supported-versions +* Python 3.8+ +* Django 3-5 Additionally, each backend has its own requirements. You should refer to https://django-haystack.readthedocs.io/en/latest/installing_search_engines.html for more diff --git a/docs/changelog.rst b/docs/changelog.rst index 00a749710..132326683 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -900,7 +900,7 @@ Other Add python 3.5 to tests - Add python 3.5 to tests. [Marco Badan] - ref: https://docs.djangoproject.com/en/1.9/faq/install/#what-python-version-can-i-use-with-django + ref: https://docs.djangoproject.com/en/stable/faq/install/#what-python-version-can-i-use-with-django - SearchQuerySet: don’t trigger backend access in __repr__ [Chris Adams] This can lead to confusing errors or performance issues by diff --git a/docs/conf.py b/docs/conf.py index 3b46fa208..d8239e5a2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,8 +10,6 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import os -import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/docs/contributing.rst b/docs/contributing.rst index c1ca45c26..7d8f0934f 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -115,7 +115,7 @@ If you've been granted the commit bit, here's how to shepherd the changes in: * ``git merge --squash`` is a good tool for performing this, as is ``git rebase -i HEAD~N``. - * This is done to prevent anyone using the git repo from accidently pulling + * This is done to prevent anyone using the git repo from accidentally pulling work-in-progress commits. * Commit messages should use past tense, describe what changed & thank anyone diff --git a/docs/haystack_theme/layout.html b/docs/haystack_theme/layout.html index b342cb597..2cf423bf3 100644 --- a/docs/haystack_theme/layout.html +++ b/docs/haystack_theme/layout.html @@ -1,7 +1,7 @@ {% extends "basic/layout.html" %} {%- block extrahead %} - + {% endblock %} diff --git a/docs/index.rst b/docs/index.rst index f5267e9c2..7c3a96e10 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -116,8 +116,8 @@ Requirements Haystack has a relatively easily-met set of requirements. -* Python 2.7+ or Python 3.3+ -* A supported version of Django: https://www.djangoproject.com/download/#supported-versions +* Python 3.8+ +* Django 3-5 Additionally, each backend has its own requirements. You should refer to :doc:`installing_search_engines` for more details. diff --git a/docs/python3.rst b/docs/python3.rst index 310ced294..ec5e8874e 100644 --- a/docs/python3.rst +++ b/docs/python3.rst @@ -15,7 +15,7 @@ Virtually all tests pass under both Python 2 & 3, with a small number of expected failures under Python (typically related to ordering, see below). .. _`six`: http://pythonhosted.org/six/ -.. _`Django`: https://docs.djangoproject.com/en/1.5/topics/python3/#str-and-unicode-methods +.. _`Django`: https://docs.djangoproject.com/en/stable/topics/python3/#str-and-unicode-methods Supported Backends diff --git a/docs/running_tests.rst b/docs/running_tests.rst index 76d4daea8..0f88ba2e1 100644 --- a/docs/running_tests.rst +++ b/docs/running_tests.rst @@ -29,17 +29,13 @@ the errors persist. To run just a portion of the tests you can use the script ``run_tests.py`` and just specify the files or directories you wish to run, for example:: - cd test_haystack - ./run_tests.py whoosh_tests test_loading.py + python test_haystack/run_tests.py whoosh_tests test_loading.py -The ``run_tests.py`` script is just a tiny wrapper around the nose_ library and -any options you pass to it will be passed on; including ``--help`` to get a -list of possible options:: +The ``run_tests.py`` script is just a tiny wrapper around the Django test +command and any options you pass to it will be passed on; including ``--help`` +to get a list of possible options:: - cd test_haystack - ./run_tests.py --help - -.. _nose: https://nose.readthedocs.io/en/latest/ + python test_haystack/run_tests.py --help Configuring Solr ================ @@ -67,4 +63,4 @@ If you want to run the geo-django tests you may need to review the cd test_haystack ./run_tests.py elasticsearch_tests -.. _GeoDjango GEOS and GDAL settings: https://docs.djangoproject.com/en/1.7/ref/contrib/gis/install/geolibs/#geos-library-path +.. _GeoDjango GEOS and GDAL settings: https://docs.djangoproject.com/en/stable/ref/contrib/gis/install/geolibs/#geos-library-path diff --git a/docs/searchindex_api.rst b/docs/searchindex_api.rst index 3f32c1b24..a537e1cda 100644 --- a/docs/searchindex_api.rst +++ b/docs/searchindex_api.rst @@ -352,7 +352,7 @@ non-existent), merely an example of how to extend existing fields. .. note:: - This method is analagous to Django's ``Field.clean`` methods. + This method is analogous to Django's ``Field.clean`` methods. Adding New Fields diff --git a/docs/spatial.rst b/docs/spatial.rst index 34227fa85..3f4b8e028 100644 --- a/docs/spatial.rst +++ b/docs/spatial.rst @@ -14,7 +14,7 @@ close to GeoDjango_ as possible. There are some differences, which we'll highlight throughout this guide. Additionally, while the support isn't as comprehensive as PostGIS (for example), it is still quite useful. -.. _GeoDjango: https://docs.djangoproject.com/en/1.11/ref/contrib/gis/ +.. _GeoDjango: https://docs.djangoproject.com/en/stable/ref/contrib/gis/ Additional Requirements @@ -261,7 +261,8 @@ calculations on your part. Examples:: from haystack.query import SearchQuerySet - from django.contrib.gis.geos import Point, D + from django.contrib.gis.geos import Point + from django.contrib.gis.measure import D ninth_and_mass = Point(-95.23592948913574, 38.96753407043678) # Within a two miles. @@ -304,7 +305,8 @@ include these calculated distances on results. Examples:: from haystack.query import SearchQuerySet - from django.contrib.gis.geos import Point, D + from django.contrib.gis.geos import Point + from django.contrib.gis.measure import D ninth_and_mass = Point(-95.23592948913574, 38.96753407043678) @@ -322,7 +324,8 @@ key, well-cached hotspots in town but want distances from the user's current position:: from haystack.query import SearchQuerySet - from django.contrib.gis.geos import Point, D + from django.contrib.gis.geos import Point + from django.contrib.gis.measure import D ninth_and_mass = Point(-95.23592948913574, 38.96753407043678) user_loc = Point(-95.23455619812012, 38.97240128290697) @@ -363,7 +366,8 @@ distance information on the results & nothing to sort by. Examples:: from haystack.query import SearchQuerySet - from django.contrib.gis.geos import Point, D + from django.contrib.gis.geos import Point + from django.contrib.gis.measure import D ninth_and_mass = Point(-95.23592948913574, 38.96753407043678) downtown_bottom_left = Point(-95.23947, 38.9637903) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index b902b7894..d3228beea 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -54,7 +54,7 @@ note-taking application. Here is ``myapp/models.py``:: title = models.CharField(max_length=200) body = models.TextField() - def __unicode__(self): + def __str__(self): return self.title Finally, before starting with Haystack, you will want to choose a search diff --git a/docs/views_and_forms.rst b/docs/views_and_forms.rst index 0edeeeb54..7f518e79b 100644 --- a/docs/views_and_forms.rst +++ b/docs/views_and_forms.rst @@ -11,7 +11,7 @@ Views & Forms which use the standard Django `class-based views`_ which are available in every version of Django which is supported by Haystack. -.. _class-based views: https://docs.djangoproject.com/en/1.7/topics/class-based-views/ +.. _class-based views: https://docs.djangoproject.com/en/stable/topics/class-based-views/ Haystack comes with some default, simple views & forms as well as some django-style views to help you get started and to cover the common cases. @@ -137,7 +137,7 @@ Views which use the standard Django `class-based views`_ which are available in every version of Django which is supported by Haystack. -.. _class-based views: https://docs.djangoproject.com/en/1.7/topics/class-based-views/ +.. _class-based views: https://docs.djangoproject.com/en/stable/topics/class-based-views/ New Django Class Based Views ---------------------------- @@ -145,7 +145,7 @@ New Django Class Based Views .. versionadded:: 2.4.0 The views in ``haystack.generic_views.SearchView`` inherit from Django’s standard -`FormView `_. +`FormView `_. The example views can be customized like any other Django class-based view as demonstrated in this example which filters the search results in ``get_queryset``:: @@ -232,9 +232,9 @@ preprocess the values returned by Haystack, that code would move to ``get_contex | ``get_query()`` | `get_queryset()`_ | +-----------------------+-------------------------------------------+ -.. _get_context_data(): https://docs.djangoproject.com/en/1.7/ref/class-based-views/mixins-simple/#django.views.generic.base.ContextMixin.get_context_data -.. _dispatch(): https://docs.djangoproject.com/en/1.7/ref/class-based-views/base/#django.views.generic.base.View.dispatch -.. _get_queryset(): https://docs.djangoproject.com/en/1.7/ref/class-based-views/mixins-multiple-object/#django.views.generic.list.MultipleObjectMixin.get_queryset +.. _get_context_data(): https://docs.djangoproject.com/en/stable/ref/class-based-views/mixins-simple/#django.views.generic.base.ContextMixin.get_context_data +.. _dispatch(): https://docs.djangoproject.com/en/stable/ref/class-based-views/base/#django.views.generic.base.View.dispatch +.. _get_queryset(): https://docs.djangoproject.com/en/stable/ref/class-based-views/mixins-multiple-object/#django.views.generic.list.MultipleObjectMixin.get_queryset Old-Style Views diff --git a/example_project/regular_app/models.py b/example_project/regular_app/models.py index e1a075e69..854ab2c26 100644 --- a/example_project/regular_app/models.py +++ b/example_project/regular_app/models.py @@ -36,7 +36,7 @@ def full_name(self): class Toy(models.Model): - dog = models.ForeignKey(Dog, related_name="toys") + dog = models.ForeignKey(Dog, on_delete=models.CASCADE, related_name="toys") name = models.CharField(max_length=60) def __str__(self): diff --git a/haystack/__init__.py b/haystack/__init__.py index 94b8f4674..4f427573d 100644 --- a/haystack/__init__.py +++ b/haystack/__init__.py @@ -1,7 +1,8 @@ -import django +from importlib.metadata import PackageNotFoundError, version + from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from pkg_resources import DistributionNotFound, get_distribution, parse_version +from packaging.version import Version from haystack.constants import DEFAULT_ALIAS from haystack.utils import loading @@ -9,18 +10,11 @@ __author__ = "Daniel Lindsley" try: - pkg_distribution = get_distribution("django-haystack") - __version__ = pkg_distribution.version - version_info = pkg_distribution.parsed_version -except DistributionNotFound: + __version__ = version("django-haystack") + version_info = Version(__version__) +except PackageNotFoundError: __version__ = "0.0.dev0" - version_info = parse_version(__version__) - - -if django.VERSION < (3, 2): - # default_app_config is deprecated since django 3.2. - default_app_config = "haystack.apps.HaystackConfig" - + version_info = Version(__version__) # Help people clean up from 1.X. if hasattr(settings, "HAYSTACK_SITECONF"): diff --git a/haystack/admin.py b/haystack/admin.py index feeb1f3f3..3f2fd0c19 100644 --- a/haystack/admin.py +++ b/haystack/admin.py @@ -1,3 +1,4 @@ +from django import VERSION as django_version from django.contrib.admin.options import ModelAdmin, csrf_protect_m from django.contrib.admin.views.main import SEARCH_VAR, ChangeList from django.core.exceptions import PermissionDenied @@ -15,7 +16,10 @@ class SearchChangeList(ChangeList): def __init__(self, **kwargs): self.haystack_connection = kwargs.pop("haystack_connection", DEFAULT_ALIAS) - super().__init__(**kwargs) + super_kwargs = kwargs + if django_version[0] >= 4: + super_kwargs["search_help_text"] = "Search..." + super().__init__(**super_kwargs) def get_results(self, request): if SEARCH_VAR not in request.GET: diff --git a/haystack/backends/elasticsearch2_backend.py b/haystack/backends/elasticsearch2_backend.py index 97c8cca15..ce744107f 100644 --- a/haystack/backends/elasticsearch2_backend.py +++ b/haystack/backends/elasticsearch2_backend.py @@ -79,21 +79,17 @@ def clear(self, models=None, commit=True): ) self.conn.indices.refresh(index=self.index_name) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise if models is not None: - self.log.error( - "Failed to clear Elasticsearch index of models '%s': %s", + self.log.exception( + "Failed to clear Elasticsearch index of models '%s'", ",".join(models_to_delete), - e, - exc_info=True, ) else: - self.log.error( - "Failed to clear Elasticsearch index: %s", e, exc_info=True - ) + self.log.exception("Failed to clear Elasticsearch index") def build_search_kwargs( self, @@ -321,15 +317,13 @@ def more_like_this( **self._get_doc_type_option(), **params, ) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise - self.log.error( - "Failed to fetch More Like This from Elasticsearch for document '%s': %s", + self.log.exception( + "Failed to fetch More Like This from Elasticsearch for document '%s'", doc_id, - e, - exc_info=True, ) raw_results = {} diff --git a/haystack/backends/elasticsearch5_backend.py b/haystack/backends/elasticsearch5_backend.py index 2eedc1ad3..3afe11347 100644 --- a/haystack/backends/elasticsearch5_backend.py +++ b/haystack/backends/elasticsearch5_backend.py @@ -75,21 +75,17 @@ def clear(self, models=None, commit=True): ) self.conn.indices.refresh(index=self.index_name) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise if models is not None: - self.log.error( - "Failed to clear Elasticsearch index of models '%s': %s", + self.log.exception( + "Failed to clear Elasticsearch index of models '%s'", ",".join(models_to_delete), - e, - exc_info=True, ) else: - self.log.error( - "Failed to clear Elasticsearch index: %s", e, exc_info=True - ) + self.log.exception("Failed to clear Elasticsearch index") def build_search_kwargs( self, @@ -411,15 +407,13 @@ def more_like_this( **self._get_doc_type_option(), **params, ) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise - self.log.error( - "Failed to fetch More Like This from Elasticsearch for document '%s': %s", + self.log.exception( + "Failed to fetch More Like This from Elasticsearch for document '%s'", doc_id, - e, - exc_info=True, ) raw_results = {} diff --git a/haystack/backends/elasticsearch7_backend.py b/haystack/backends/elasticsearch7_backend.py index dd9c9933d..f8d7d767e 100644 --- a/haystack/backends/elasticsearch7_backend.py +++ b/haystack/backends/elasticsearch7_backend.py @@ -27,29 +27,6 @@ ) -DEFAULT_FIELD_MAPPING = { - "type": "text", - "analyzer": "snowball", -} -FIELD_MAPPINGS = { - "edge_ngram": { - "type": "text", - "analyzer": "edgengram_analyzer", - }, - "ngram": { - "type": "text", - "analyzer": "ngram_analyzer", - }, - "date": {"type": "date"}, - "datetime": {"type": "date"}, - "location": {"type": "geo_point"}, - "boolean": {"type": "boolean"}, - "float": {"type": "float"}, - "long": {"type": "long"}, - "integer": {"type": "long"}, -} - - class Elasticsearch7SearchBackend(ElasticsearchSearchBackend): # Settings to add an n-gram & edge n-gram analyzer. DEFAULT_SETTINGS = { @@ -90,6 +67,29 @@ class Elasticsearch7SearchBackend(ElasticsearchSearchBackend): }, } + DEFAULT_FIELD_MAPPING = { + "type": "text", + "analyzer": "snowball", + } + + FIELD_MAPPINGS = { + "edge_ngram": { + "type": "text", + "analyzer": "edgengram_analyzer", + }, + "ngram": { + "type": "text", + "analyzer": "ngram_analyzer", + }, + "date": {"type": "date"}, + "datetime": {"type": "date"}, + "location": {"type": "geo_point"}, + "boolean": {"type": "boolean"}, + "float": {"type": "float"}, + "long": {"type": "long"}, + "integer": {"type": "long"}, + } + def __init__(self, connection_alias, **connection_options): super().__init__(connection_alias, **connection_options) self.content_field_name = None @@ -143,21 +143,17 @@ def clear(self, models=None, commit=True): ) self.conn.indices.refresh(index=self.index_name) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise if models is not None: - self.log.error( - "Failed to clear Elasticsearch index of models '%s': %s", + self.log.exception( + "Failed to clear Elasticsearch index of models '%s'", ",".join(models_to_delete), - e, - exc_info=True, ) else: - self.log.error( - "Failed to clear Elasticsearch index: %s", e, exc_info=True - ) + self.log.exception("Failed to clear Elasticsearch index") def build_search_kwargs( self, @@ -479,15 +475,13 @@ def more_like_this( raw_results = self.conn.search( body=mlt_query, index=self.index_name, _source=True, **params ) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise - self.log.error( - "Failed to fetch More Like This from Elasticsearch for document '%s': %s", + self.log.exception( + "Failed to fetch More Like This from Elasticsearch for document '%s'", doc_id, - e, - exc_info=True, ) raw_results = {} @@ -556,8 +550,8 @@ def build_schema(self, fields): mapping = self._get_common_mapping() for _, field_class in fields.items(): - field_mapping = FIELD_MAPPINGS.get( - field_class.field_type, DEFAULT_FIELD_MAPPING + field_mapping = self.FIELD_MAPPINGS.get( + field_class.field_type, self.DEFAULT_FIELD_MAPPING ).copy() if field_class.boost != 1.0: field_mapping["boost"] = field_class.boost diff --git a/haystack/backends/elasticsearch_backend.py b/haystack/backends/elasticsearch_backend.py index c2fb47f5f..e8febf9d3 100644 --- a/haystack/backends/elasticsearch_backend.py +++ b/haystack/backends/elasticsearch_backend.py @@ -199,13 +199,11 @@ def update(self, index, iterable, commit=True): if not self.setup_complete: try: self.setup() - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise - self.log.error( - "Failed to add documents to Elasticsearch: %s", e, exc_info=True - ) + self.log.exception("Failed to add documents to Elasticsearch") return prepped_docs = [] @@ -223,16 +221,15 @@ def update(self, index, iterable, commit=True): prepped_docs.append(final_data) except SkipDocument: self.log.debug("Indexing for object `%s` skipped", obj) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise # We'll log the object identifier but won't include the actual object # to avoid the possibility of that generating encoding errors while # processing the log message: - self.log.error( - "%s while preparing object for update" % e.__class__.__name__, - exc_info=True, + self.log.exception( + "Preparing object for update", extra={"data": {"index": index, "object": get_identifier(obj)}}, ) @@ -252,15 +249,13 @@ def remove(self, obj_or_string, commit=True): if not self.setup_complete: try: self.setup() - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise - self.log.error( - "Failed to remove document '%s' from Elasticsearch: %s", + self.log.exception( + "Failed to remove document '%s' from Elasticsearch", doc_id, - e, - exc_info=True, ) return @@ -274,15 +269,13 @@ def remove(self, obj_or_string, commit=True): if commit: self.conn.indices.refresh(index=self.index_name) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise - self.log.error( - "Failed to remove document '%s' from Elasticsearch: %s", + self.log.exception( + "Failed to remove document '%s' from Elasticsearch", doc_id, - e, - exc_info=True, ) def clear(self, models=None, commit=True): @@ -305,7 +298,7 @@ def clear(self, models=None, commit=True): for model in models: models_to_delete.append("%s:%s" % (DJANGO_CT, get_model_ct(model))) - # Delete by query in Elasticsearch asssumes you're dealing with + # Delete by query in Elasticsearch assumes you're dealing with # a ``query`` root object. :/ query = { "query": {"query_string": {"query": " OR ".join(models_to_delete)}} @@ -315,21 +308,17 @@ def clear(self, models=None, commit=True): body=query, **self._get_doc_type_option(), ) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise if models is not None: - self.log.error( - "Failed to clear Elasticsearch index of models '%s': %s", + self.log.exception( + "Failed to clear Elasticsearch index of models '%s'", ",".join(models_to_delete), - e, - exc_info=True, ) else: - self.log.error( - "Failed to clear Elasticsearch index: %s", e, exc_info=True - ) + self.log.exception("Failed to clear Elasticsearch index") def build_search_kwargs( self, @@ -588,15 +577,13 @@ def search(self, query_string, **kwargs): _source=True, **self._get_doc_type_option(), ) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise - self.log.error( - "Failed to query Elasticsearch using '%s': %s", + self.log.exception( + "Failed to query Elasticsearch using '%s'", query_string, - e, - exc_info=True, ) raw_results = {} @@ -652,15 +639,13 @@ def more_like_this( **self._get_doc_type_option(), **params, ) - except elasticsearch.TransportError as e: + except elasticsearch.TransportError: if not self.silently_fail: raise - self.log.error( - "Failed to fetch More Like This from Elasticsearch for document '%s': %s", + self.log.exception( + "Failed to fetch More Like This from Elasticsearch for document '%s'", doc_id, - e, - exc_info=True, ) raw_results = {} @@ -692,9 +677,11 @@ def _process_results( if raw_suggest: spelling_suggestion = " ".join( [ - word["text"] - if len(word["options"]) == 0 - else word["options"][0]["text"] + ( + word["text"] + if len(word["options"]) == 0 + else word["options"][0]["text"] + ) for word in raw_suggest ] ) @@ -971,7 +958,7 @@ def build_query_fragment(self, field, filter_type, value): if value.input_type_name == "exact": query_frag = prepared_value else: - # Iterate over terms & incorportate the converted form of each into the query. + # Iterate over terms & incorporate the converted form of each into the query. terms = [] if isinstance(prepared_value, str): diff --git a/haystack/backends/simple_backend.py b/haystack/backends/simple_backend.py index a3bb59400..bfef88cb2 100644 --- a/haystack/backends/simple_backend.py +++ b/haystack/backends/simple_backend.py @@ -1,6 +1,7 @@ """ A very basic, ORM-based backend for simple search during tests. """ + from functools import reduce from warnings import warn @@ -56,7 +57,7 @@ def search(self, query_string, **kwargs): if hasattr(field, "related"): continue - if not field.get_internal_type() in ( + if field.get_internal_type() not in ( "TextField", "CharField", "SlugField", diff --git a/haystack/backends/solr_backend.py b/haystack/backends/solr_backend.py index dc929bf33..e077aa302 100644 --- a/haystack/backends/solr_backend.py +++ b/haystack/backends/solr_backend.py @@ -91,20 +91,19 @@ def update(self, index, iterable, commit=True): # We'll log the object identifier but won't include the actual object # to avoid the possibility of that generating encoding errors while # processing the log message: - self.log.error( + self.log.exception( "UnicodeDecodeError while preparing object for update", - exc_info=True, extra={"data": {"index": index, "object": get_identifier(obj)}}, ) if len(docs) > 0: try: self.conn.add(docs, commit=commit, boost=index.get_field_weights()) - except (IOError, SolrError) as e: + except (IOError, SolrError): if not self.silently_fail: raise - self.log.error("Failed to add documents to Solr: %s", e, exc_info=True) + self.log.exception("Failed to add documents to Solr") def remove(self, obj_or_string, commit=True): solr_id = get_identifier(obj_or_string) @@ -112,15 +111,13 @@ def remove(self, obj_or_string, commit=True): try: kwargs = {"commit": commit, "id": solr_id} self.conn.delete(**kwargs) - except (IOError, SolrError) as e: + except (IOError, SolrError): if not self.silently_fail: raise - self.log.error( - "Failed to remove document '%s' from Solr: %s", + self.log.exception( + "Failed to remove document '%s' from Solr", solr_id, - e, - exc_info=True, ) def clear(self, models=None, commit=True): @@ -142,19 +139,17 @@ def clear(self, models=None, commit=True): if commit: # Run an optimize post-clear. http://wiki.apache.org/solr/FAQ#head-9aafb5d8dff5308e8ea4fcf4b71f19f029c4bb99 self.conn.optimize() - except (IOError, SolrError) as e: + except (IOError, SolrError): if not self.silently_fail: raise if models is not None: - self.log.error( - "Failed to clear Solr index of models '%s': %s", + self.log.exception( + "Failed to clear Solr index of models '%s'", ",".join(models_to_delete), - e, - exc_info=True, ) else: - self.log.error("Failed to clear Solr index: %s", e, exc_info=True) + self.log.exception("Failed to clear Solr index") @log_query def search(self, query_string, **kwargs): @@ -165,13 +160,11 @@ def search(self, query_string, **kwargs): try: raw_results = self.conn.search(query_string, **search_kwargs) - except (IOError, SolrError) as e: + except (IOError, SolrError): if not self.silently_fail: raise - self.log.error( - "Failed to query Solr using '%s': %s", query_string, e, exc_info=True - ) + self.log.exception("Failed to query Solr using '%s'", query_string) raw_results = EmptyResults() return self._process_results( @@ -204,7 +197,6 @@ def build_search_kwargs( collate=None, **extra_kwargs ): - index = haystack.connections[self.connection_alias].get_unified_index() kwargs = {"fl": "* score", "df": index.document_field} @@ -275,9 +267,9 @@ def build_search_kwargs( for facet_field, options in facets.items(): for key, value in options.items(): - kwargs[ - "f.%s.facet.%s" % (facet_field, key) - ] = self.conn._from_python(value) + kwargs["f.%s.facet.%s" % (facet_field, key)] = ( + self.conn._from_python(value) + ) if date_facets is not None: kwargs["facet"] = "on" @@ -285,23 +277,24 @@ def build_search_kwargs( kwargs["facet.%s.other" % self.date_facet_field] = "none" for key, value in date_facets.items(): - kwargs[ - "f.%s.facet.%s.start" % (key, self.date_facet_field) - ] = self.conn._from_python(value.get("start_date")) - kwargs[ - "f.%s.facet.%s.end" % (key, self.date_facet_field) - ] = self.conn._from_python(value.get("end_date")) + kwargs["f.%s.facet.%s.start" % (key, self.date_facet_field)] = ( + self.conn._from_python(value.get("start_date")) + ) + kwargs["f.%s.facet.%s.end" % (key, self.date_facet_field)] = ( + self.conn._from_python(value.get("end_date")) + ) gap_by_string = value.get("gap_by").upper() gap_string = "%d%s" % (value.get("gap_amount"), gap_by_string) if value.get("gap_amount") != 1: gap_string += "S" - kwargs[ - "f.%s.facet.%s.gap" % (key, self.date_facet_field) - ] = "+%s/%s" % ( - gap_string, - gap_by_string, + kwargs["f.%s.facet.%s.gap" % (key, self.date_facet_field)] = ( + "+%s/%s" + % ( + gap_string, + gap_by_string, + ) ) if query_facets is not None: @@ -450,15 +443,12 @@ def more_like_this( try: raw_results = self.conn.more_like_this(query, field_name, **params) - except (IOError, SolrError) as e: + except (IOError, SolrError): if not self.silently_fail: raise - self.log.error( - "Failed to fetch More Like This from Solr for document '%s': %s", - query, - e, - exc_info=True, + self.log.exception( + "Failed to fetch More Like This from Solr for document '%s'", query ) raw_results = EmptyResults() @@ -514,11 +504,9 @@ def _process_results( if self.include_spelling and hasattr(raw_results, "spellcheck"): try: spelling_suggestions = self.extract_spelling_suggestions(raw_results) - except Exception as exc: - self.log.error( + except Exception: + self.log.exception( "Error extracting spelling suggestions: %s", - exc, - exc_info=True, extra={"data": {"spellcheck": raw_results.spellcheck}}, ) @@ -747,11 +735,9 @@ def extract_file_contents(self, file_obj, **kwargs): try: return self.conn.extract(file_obj, **kwargs) - except Exception as e: + except Exception: self.log.warning( - "Unable to extract file contents: %s", - e, - exc_info=True, + "Unable to extract file contents", extra={"data": {"file": file_obj}}, ) return None @@ -819,7 +805,7 @@ def build_query_fragment(self, field, filter_type, value): if value.input_type_name == "exact": query_frag = prepared_value else: - # Iterate over terms & incorportate the converted form of each into the query. + # Iterate over terms & incorporate the converted form of each into the query. terms = [] for possible_value in prepared_value.split(" "): diff --git a/haystack/backends/whoosh_backend.py b/haystack/backends/whoosh_backend.py index 5c06e8750..13d68035c 100644 --- a/haystack/backends/whoosh_backend.py +++ b/haystack/backends/whoosh_backend.py @@ -4,10 +4,10 @@ import shutil import threading import warnings +from datetime import date, datetime from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils.datetime_safe import date, datetime from django.utils.encoding import force_str from haystack.backends import ( @@ -130,7 +130,13 @@ def setup(self): # Make sure the index is there. if self.use_file_storage and not os.path.exists(self.path): - os.makedirs(self.path) + try: + os.makedirs(self.path) + except Exception: + raise IOError( + "The directory of your Whoosh index '%s' (cwd='%s') cannot be created for the current user/group." + % (self.path, os.getcwd()) + ) new_index = True if self.use_file_storage and not os.access(self.path, os.W_OK): @@ -270,16 +276,15 @@ def update(self, index, iterable, commit=True): try: writer.update_document(**doc) - except Exception as e: + except Exception: if not self.silently_fail: raise # We'll log the object identifier but won't include the actual object # to avoid the possibility of that generating encoding errors while # processing the log message: - self.log.error( - "%s while preparing object for update" % e.__class__.__name__, - exc_info=True, + self.log.exception( + "Preparing object for update", extra={"data": {"index": index, "object": get_identifier(obj)}}, ) @@ -298,15 +303,13 @@ def remove(self, obj_or_string, commit=True): try: self.index.delete_by_query(q=self.parser.parse('%s:"%s"' % (ID, whoosh_id))) - except Exception as e: + except Exception: if not self.silently_fail: raise - self.log.error( - "Failed to remove document '%s' from Whoosh: %s", + self.log.exception( + "Failed to remove document '%s' from Whoosh", whoosh_id, - e, - exc_info=True, ) def clear(self, models=None, commit=True): @@ -330,19 +333,17 @@ def clear(self, models=None, commit=True): self.index.delete_by_query( q=self.parser.parse(" OR ".join(models_to_delete)) ) - except Exception as e: + except Exception: if not self.silently_fail: raise if models is not None: - self.log.error( - "Failed to clear Whoosh index of models '%s': %s", + self.log.exception( + "Failed to clear Whoosh index of models '%s'", ",".join(models_to_delete), - e, - exc_info=True, ) else: - self.log.error("Failed to clear Whoosh index: %s", e, exc_info=True) + self.log.exception("Failed to clear Whoosh index") def delete_index(self): # Per the Whoosh mailing list, if wiping out everything from the index, @@ -929,8 +930,7 @@ class WhooshSearchQuery(BaseSearchQuery): def _convert_datetime(self, date): if hasattr(date, "hour"): return force_str(date.strftime("%Y%m%d%H%M%S")) - else: - return force_str(date.strftime("%Y%m%d000000")) + return force_str(date.strftime("%Y%m%d000000")) def clean(self, query_fragment): """ @@ -1019,7 +1019,7 @@ def build_query_fragment(self, field, filter_type, value): if value.input_type_name == "exact": query_frag = prepared_value else: - # Iterate over terms & incorportate the converted form of each into the query. + # Iterate over terms & incorporate the converted form of each into the query. terms = [] if isinstance(prepared_value, str): diff --git a/haystack/fields.py b/haystack/fields.py index 0965377ea..3531bf31b 100644 --- a/haystack/fields.py +++ b/haystack/fields.py @@ -1,8 +1,8 @@ +import datetime import re from inspect import ismethod from django.template import loader -from django.utils import datetime_safe from haystack.exceptions import SearchFieldError from haystack.utils import get_model_ct_tuple @@ -395,7 +395,7 @@ def convert(self, value): if match: data = match.groupdict() - return datetime_safe.date( + return datetime.date( int(data["year"]), int(data["month"]), int(data["day"]) ) else: @@ -428,7 +428,7 @@ def convert(self, value): if match: data = match.groupdict() - return datetime_safe.datetime( + return datetime.datetime( int(data["year"]), int(data["month"]), int(data["day"]), diff --git a/haystack/generic_views.py b/haystack/generic_views.py index 2b981a4d1..655ea4f74 100644 --- a/haystack/generic_views.py +++ b/haystack/generic_views.py @@ -128,8 +128,7 @@ def get(self, request, *args, **kwargs): if form.is_valid(): return self.form_valid(form) - else: - return self.form_invalid(form) + return self.form_invalid(form) class FacetedSearchView(FacetedSearchMixin, SearchView): diff --git a/haystack/management/commands/update_index.py b/haystack/management/commands/update_index.py index da50644bc..070332ff8 100644 --- a/haystack/management/commands/update_index.py +++ b/haystack/management/commands/update_index.py @@ -81,7 +81,6 @@ def do_update( max_retries=DEFAULT_MAX_RETRIES, last_max_pk=None, ): - # Get a clone of the QuerySet so that the cache doesn't bloat up # in memory. Useful when reindexing large amounts of data. # the query must be ordered by PK in order to get the max PK in each batch @@ -144,7 +143,7 @@ def do_update( error_msg += " (pid %(pid)s): %(exc)s" if retries >= max_retries: - LOG.error(error_msg, error_context, exc_info=True) + LOG.exception(error_msg, error_context) raise elif verbosity >= 2: LOG.warning(error_msg, error_context, exc_info=True) diff --git a/haystack/query.py b/haystack/query.py index 1be64658f..a3cf9490c 100644 --- a/haystack/query.py +++ b/haystack/query.py @@ -172,7 +172,6 @@ def post_process_results(self, results): for result in results: if self._load_all: - model_objects = loaded_objects.get(result.model, {}) # Try to coerce a primary key object that matches the models pk # We have to deal with semi-arbitrary keys being cast from strings (UUID, int, etc) @@ -314,8 +313,7 @@ def __getitem__(self, k): # Cache should be full enough for our needs. if is_slice: return self._result_cache[start:bound] - else: - return self._result_cache[start] + return self._result_cache[start] # Methods that return a SearchQuerySet. def all(self): # noqa A003 @@ -330,8 +328,7 @@ def filter(self, *args, **kwargs): # noqa A003 """Narrows the search based on certain attributes and the default operator.""" if DEFAULT_OPERATOR == "OR": return self.filter_or(*args, **kwargs) - else: - return self.filter_and(*args, **kwargs) + return self.filter_and(*args, **kwargs) def exclude(self, *args, **kwargs): """Narrows the search by ensuring certain attributes are not included.""" diff --git a/haystack/templatetags/more_like_this.py b/haystack/templatetags/more_like_this.py index 2cc22751d..3f710e9a0 100644 --- a/haystack/templatetags/more_like_this.py +++ b/haystack/templatetags/more_like_this.py @@ -42,9 +42,11 @@ def render(self, context): sqs = sqs[: self.limit] context[self.varname] = sqs - except Exception as exc: - logging.warning( - "Unhandled exception rendering %r: %s", self, exc, exc_info=True + except Exception: + logging.exception( + "Unhandled exception rendering %r", + self, + level=logging.WARNING, ) return "" @@ -73,7 +75,7 @@ def more_like_this(parser, token): """ bits = token.split_contents() - if not len(bits) in (4, 6, 8): + if len(bits) not in (4, 6, 8): raise template.TemplateSyntaxError( "'%s' tag requires either 3, 5 or 7 arguments." % bits[0] ) diff --git a/haystack/utils/__init__.py b/haystack/utils/__init__.py index b0b0d082a..18d939c41 100644 --- a/haystack/utils/__init__.py +++ b/haystack/utils/__init__.py @@ -4,7 +4,7 @@ from django.conf import settings from haystack.constants import DJANGO_CT, DJANGO_ID, ID -from haystack.utils.highlighting import Highlighter # noqa=F401 +from haystack.utils.highlighting import Highlighter # noqa: F401 IDENTIFIER_REGEX = re.compile(r"^[\w\d_]+\.[\w\d_]+\.[\w\d-]+$") diff --git a/haystack/utils/loading.py b/haystack/utils/loading.py index 216e485a1..d96af7125 100644 --- a/haystack/utils/loading.py +++ b/haystack/utils/loading.py @@ -338,7 +338,6 @@ def get_index_fieldname(self, field): return self._fieldnames.get(field) or field def get_index(self, model_klass): - indexes = self.get_indexes() if model_klass not in indexes: diff --git a/pyproject.toml b/pyproject.toml index 403009f96..da82ce895 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,103 @@ [build-system] -requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61.2", + "setuptools_scm[toml]>=3.4", + "wheel", +] -[tool.black] -line_length=88 +[project] +name = "django-haystack" +description = "Pluggable search for Django." +readme = "README.rst" +authors = [{name = "Daniel Lindsley", email = "daniel@toastdriven.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Utilities", +] +dynamic = [ + "version", +] +dependencies = [ + "Django>=3.2", + "packaging", +] +[project.optional-dependencies] +elasticsearch = [ + "elasticsearch<8,>=5", +] +testing = [ + "coverage", + "geopy==2", + "pysolr>=3.7", + "python-dateutil", + "requests", + "whoosh<3.0,>=2.5.4", +] +[project.urls] +Documentation = "https://django-haystack.readthedocs.io" +Homepage = "http://haystacksearch.org/" +Source = "https://github.com/django-haystack/django-haystack" + +[tool.setuptools] +packages = [ + "haystack", + "haystack.backends", + "haystack.management", + "haystack.management.commands", + "haystack.templatetags", + "haystack.utils", +] +include-package-data = false +# test-suite = "test_haystack.run_tests.run_all" # validate-pyproject-toml will complain +zip-safe = false + +[tool.setuptools.package-data] +haystack = [ + "templates/panels/*", + "templates/search_configuration/*", +] + +[tool.setuptools_scm] +fallback_version = "0.0.dev0" +write_to = "haystack/version.py" [tool.isort] known_first_party = ["haystack", "test_haystack"] profile = "black" multi_line_output = 3 -[tool.setuptools_scm] -fallback_version = "0.0.dev0" -write_to = "haystack/version.py" +[tool.ruff] +exclude = ["test_haystack"] +ignore = ["B018", "B028", "B904", "B905"] +line-length = 162 +select = ["ASYNC", "B", "C4", "DJ", "E", "F", "G", "PLR091", "W"] +show-source = true +target-version = "py38" + +[tool.ruff.isort] +known-first-party = ["haystack", "test_haystack"] + +[tool.ruff.mccabe] +max-complexity = 14 + +[tool.ruff.pylint] +max-args = 20 +max-branches = 40 +max-returns = 8 +max-statements = 91 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index bae09868b..000000000 --- a/setup.cfg +++ /dev/null @@ -1,12 +0,0 @@ -[pep8] -line_length=88 -exclude=docs - -[flake8] -line_length=88 -exclude=docs,tests -ignore=E203, E501, W503, D - -[options] -setup_requires = - setuptools_scm diff --git a/setup.py b/setup.py deleted file mode 100644 index 5cc6d6b28..000000000 --- a/setup.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -from setuptools import setup - -install_requires = ["Django>=2.2"] - -tests_require = [ - "pysolr>=3.7.0", - "whoosh>=2.5.4,<3.0", - "python-dateutil", - "geopy==2.0.0", - "nose", - "coverage", - "requests", -] - -setup( - name="django-haystack", - use_scm_version=True, - description="Pluggable search for Django.", - author="Daniel Lindsley", - author_email="daniel@toastdriven.com", - long_description=open("README.rst", "r").read(), - url="http://haystacksearch.org/", - project_urls={ - "Documentation": "https://django-haystack.readthedocs.io", - "Source": "https://github.com/django-haystack/django-haystack", - }, - packages=[ - "haystack", - "haystack.backends", - "haystack.management", - "haystack.management.commands", - "haystack.templatetags", - "haystack.utils", - ], - package_data={ - "haystack": ["templates/panels/*", "templates/search_configuration/*"] - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: Django", - "Framework :: Django :: 2.2", - "Framework :: Django :: 3.1", - "Framework :: Django :: 3.2", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Topic :: Utilities", - ], - zip_safe=False, - install_requires=install_requires, - tests_require=tests_require, - extras_require={ - "elasticsearch": ["elasticsearch>=5,<8"], - }, - test_suite="test_haystack.run_tests.run_all", -) diff --git a/test_haystack/__init__.py b/test_haystack/__init__.py index 8e2707352..e69de29bb 100644 --- a/test_haystack/__init__.py +++ b/test_haystack/__init__.py @@ -1,27 +0,0 @@ -import os - -test_runner = None -old_config = None - -os.environ["DJANGO_SETTINGS_MODULE"] = "test_haystack.settings" - - -import django - -django.setup() - - -def setup(): - global test_runner - global old_config - - from django.test.runner import DiscoverRunner - - test_runner = DiscoverRunner() - test_runner.setup_test_environment() - old_config = test_runner.setup_databases() - - -def teardown(): - test_runner.teardown_databases(old_config) - test_runner.teardown_test_environment() diff --git a/test_haystack/core/admin.py b/test_haystack/core/admin.py index 3e374bc6b..404dbefbe 100644 --- a/test_haystack/core/admin.py +++ b/test_haystack/core/admin.py @@ -5,10 +5,8 @@ from .models import MockModel +@admin.register(MockModel) class MockModelAdmin(SearchModelAdmin): haystack_connection = "solr" date_hierarchy = "pub_date" list_display = ("author", "pub_date") - - -admin.site.register(MockModel, MockModelAdmin) diff --git a/test_haystack/elasticsearch2_tests/__init__.py b/test_haystack/elasticsearch2_tests/__init__.py index 67a9e9764..38fa24fbc 100644 --- a/test_haystack/elasticsearch2_tests/__init__.py +++ b/test_haystack/elasticsearch2_tests/__init__.py @@ -1,14 +1,12 @@ +import os import unittest -import warnings from django.conf import settings from haystack.utils import log as logging -warnings.simplefilter("ignore", Warning) - -def setup(): +def load_tests(loader, standard_tests, pattern): log = logging.getLogger("haystack") try: import elasticsearch @@ -29,3 +27,9 @@ def setup(): except exceptions.ConnectionError as e: log.error("elasticsearch not running on %r" % url, exc_info=True) raise unittest.SkipTest("elasticsearch not running on %r" % url, e) + + package_tests = loader.discover( + start_dir=os.path.dirname(__file__), pattern=pattern + ) + standard_tests.addTests(package_tests) + return standard_tests diff --git a/test_haystack/elasticsearch5_tests/__init__.py b/test_haystack/elasticsearch5_tests/__init__.py index 09f1ab176..5594ce332 100644 --- a/test_haystack/elasticsearch5_tests/__init__.py +++ b/test_haystack/elasticsearch5_tests/__init__.py @@ -1,14 +1,12 @@ +import os import unittest -import warnings from django.conf import settings from haystack.utils import log as logging -warnings.simplefilter("ignore", Warning) - -def setup(): +def load_tests(loader, standard_tests, pattern): log = logging.getLogger("haystack") try: import elasticsearch @@ -29,3 +27,9 @@ def setup(): except exceptions.ConnectionError as e: log.error("elasticsearch not running on %r" % url, exc_info=True) raise unittest.SkipTest("elasticsearch not running on %r" % url, e) + + package_tests = loader.discover( + start_dir=os.path.dirname(__file__), pattern=pattern + ) + standard_tests.addTests(package_tests) + return standard_tests diff --git a/test_haystack/elasticsearch7_tests/__init__.py b/test_haystack/elasticsearch7_tests/__init__.py index 6491d464a..24339ac89 100644 --- a/test_haystack/elasticsearch7_tests/__init__.py +++ b/test_haystack/elasticsearch7_tests/__init__.py @@ -1,14 +1,12 @@ +import os import unittest -import warnings from django.conf import settings from haystack.utils import log as logging -warnings.simplefilter("ignore", Warning) - -def setup(): +def load_tests(loader, standard_tests, pattern): log = logging.getLogger("haystack") try: import elasticsearch @@ -29,3 +27,9 @@ def setup(): except exceptions.ConnectionError as e: log.error("elasticsearch not running on %r" % url, exc_info=True) raise unittest.SkipTest("elasticsearch not running on %r" % url, e) + + package_tests = loader.discover( + start_dir=os.path.dirname(__file__), pattern=pattern + ) + standard_tests.addTests(package_tests) + return standard_tests diff --git a/test_haystack/elasticsearch_tests/__init__.py b/test_haystack/elasticsearch_tests/__init__.py index 05c53d640..0ceb159dc 100644 --- a/test_haystack/elasticsearch_tests/__init__.py +++ b/test_haystack/elasticsearch_tests/__init__.py @@ -1,14 +1,12 @@ +import os import unittest -import warnings from django.conf import settings from haystack.utils import log as logging -warnings.simplefilter("ignore", Warning) - -def setup(): +def load_tests(loader, standard_tests, pattern): log = logging.getLogger("haystack") try: import elasticsearch @@ -36,3 +34,9 @@ def setup(): % settings.HAYSTACK_CONNECTIONS["elasticsearch"]["URL"], e, ) + + package_tests = loader.discover( + start_dir=os.path.dirname(__file__), pattern=pattern + ) + standard_tests.addTests(package_tests) + return standard_tests diff --git a/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py b/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py index 665b00cea..7de53333c 100644 --- a/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py +++ b/test_haystack/elasticsearch_tests/test_elasticsearch_backend.py @@ -229,7 +229,6 @@ def test_kwargs_are_passed_on(self): class ElasticSearchMockUnifiedIndex(UnifiedIndex): - spy_args = None def get_index(self, model_klass): diff --git a/test_haystack/multipleindex/__init__.py b/test_haystack/multipleindex/__init__.py index d48e717da..0cd29ea56 100644 --- a/test_haystack/multipleindex/__init__.py +++ b/test_haystack/multipleindex/__init__.py @@ -1,24 +1,12 @@ -from django.apps import apps - -import haystack -from haystack.signals import RealtimeSignalProcessor +import os from ..utils import check_solr -_old_sp = None - -def setup(): +def load_tests(loader, standard_tests, pattern): check_solr() - global _old_sp - config = apps.get_app_config("haystack") - _old_sp = config.signal_processor - config.signal_processor = RealtimeSignalProcessor( - haystack.connections, haystack.connection_router + package_tests = loader.discover( + start_dir=os.path.dirname(__file__), pattern=pattern ) - - -def teardown(): - config = apps.get_app_config("haystack") - config.signal_processor.teardown() - config.signal_processor = _old_sp + standard_tests.addTests(package_tests) + return standard_tests diff --git a/test_haystack/multipleindex/tests.py b/test_haystack/multipleindex/tests.py index 5161a1f13..d4eda9b82 100644 --- a/test_haystack/multipleindex/tests.py +++ b/test_haystack/multipleindex/tests.py @@ -1,9 +1,10 @@ +from django.apps import apps from django.db import models -from haystack import connections +from haystack import connection_router, connections from haystack.exceptions import NotHandled from haystack.query import SearchQuerySet -from haystack.signals import BaseSignalProcessor +from haystack.signals import BaseSignalProcessor, RealtimeSignalProcessor from ..whoosh_tests.testcases import WhooshTestCase from .models import Bar, Foo @@ -191,6 +192,22 @@ def teardown(self): class SignalProcessorTestCase(WhooshTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + config = apps.get_app_config("haystack") + cls._old_sp = config.signal_processor + config.signal_processor = RealtimeSignalProcessor( + connections, connection_router + ) + + @classmethod + def tearDown(cls): + config = apps.get_app_config("haystack") + config.signal_processor.teardown() + config.signal_processor = cls._old_sp + super().tearDown() + def setUp(self): super().setUp() diff --git a/test_haystack/run_tests.py b/test_haystack/run_tests.py index 22f167637..85fa00a96 100755 --- a/test_haystack/run_tests.py +++ b/test_haystack/run_tests.py @@ -1,24 +1,17 @@ #!/usr/bin/env python +import os import sys -from os.path import abspath, dirname -import nose +import django +from django.core.management import call_command def run_all(argv=None): - sys.exitfunc = lambda: sys.stderr.write("Shutting down....\n") + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + os.environ["DJANGO_SETTINGS_MODULE"] = "test_haystack.settings" + django.setup() - # always insert coverage when running tests through setup.py - if argv is None: - argv = [ - "nosetests", - "--with-coverage", - "--cover-package=haystack", - "--cover-erase", - "--verbose", - ] - - nose.run_exit(argv=argv, defaultTest=abspath(dirname(__file__))) + call_command("test", sys.argv[1:]) if __name__ == "__main__": diff --git a/test_haystack/settings.py b/test_haystack/settings.py index c4234f547..9a78bc5bc 100644 --- a/test_haystack/settings.py +++ b/test_haystack/settings.py @@ -8,6 +8,9 @@ "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "haystack_tests.db"} } +# Use BigAutoField as the default auto field for all models +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", @@ -34,6 +37,7 @@ "APP_DIRS": True, "OPTIONS": { "context_processors": [ + "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ] diff --git a/test_haystack/simple_tests/test_simple_backend.py b/test_haystack/simple_tests/test_simple_backend.py index e19662217..3f3df65e8 100644 --- a/test_haystack/simple_tests/test_simple_backend.py +++ b/test_haystack/simple_tests/test_simple_backend.py @@ -206,7 +206,6 @@ def test_more_like_this(self): self.assertEqual(self.backend.more_like_this(self.sample_objs[0])["hits"], 0) def test_score_field_collision(self): - index = connections["simple"].get_unified_index().get_index(ScoreMockModel) sample_objs = ScoreMockModel.objects.all() diff --git a/test_haystack/solr_tests/__init__.py b/test_haystack/solr_tests/__init__.py index 1b1d43036..0cd29ea56 100644 --- a/test_haystack/solr_tests/__init__.py +++ b/test_haystack/solr_tests/__init__.py @@ -1,9 +1,12 @@ -import warnings - -warnings.simplefilter("ignore", Warning) +import os from ..utils import check_solr -def setup(): +def load_tests(loader, standard_tests, pattern): check_solr() + package_tests = loader.discover( + start_dir=os.path.dirname(__file__), pattern=pattern + ) + standard_tests.addTests(package_tests) + return standard_tests diff --git a/test_haystack/solr_tests/server/setup-solr-test-server-in-docker.sh b/test_haystack/solr_tests/server/setup-solr-test-server-in-docker.sh new file mode 100644 index 000000000..bf2b4fb9d --- /dev/null +++ b/test_haystack/solr_tests/server/setup-solr-test-server-in-docker.sh @@ -0,0 +1,15 @@ +# figure out the solr container ID +SOLR_CONTAINER=`docker ps -f ancestor=solr:6 --format '{{.ID}}'` + +LOCAL_CONFDIR=./test_haystack/solr_tests/server/confdir +CONTAINER_CONFDIR=/opt/solr/server/solr/collection1/conf + +# set up a solr core +docker exec $SOLR_CONTAINER ./bin/solr create -c collection1 -p 8983 -n basic_config +# copy the testing schema to the collection and fix permissions +docker cp $LOCAL_CONFDIR/solrconfig.xml $SOLR_CONTAINER:$CONTAINER_CONFDIR/solrconfig.xml +docker cp $LOCAL_CONFDIR/schema.xml $SOLR_CONTAINER:$CONTAINER_CONFDIR/schema.xml +docker exec $SOLR_CONTAINER mv $CONTAINER_CONFDIR/managed-schema $CONTAINER_CONFDIR/managed-schema.old +docker exec -u root $SOLR_CONTAINER chown -R solr:solr /opt/solr/server/solr/collection1 +# reload the solr core +curl "http://localhost:9001/solr/admin/cores?action=RELOAD&core=collection1" diff --git a/test_haystack/solr_tests/test_solr_backend.py b/test_haystack/solr_tests/test_solr_backend.py index d20347e7e..cc0ad551a 100644 --- a/test_haystack/solr_tests/test_solr_backend.py +++ b/test_haystack/solr_tests/test_solr_backend.py @@ -10,7 +10,7 @@ from django.conf import settings from django.test import TestCase from django.test.utils import override_settings -from pkg_resources import parse_version +from packaging.version import Version from haystack import connections, indexes, reset_search_queries from haystack.exceptions import SkipDocument @@ -420,7 +420,7 @@ def test_search(self): "results" ] ], - ["Indexed!\n1", "Indexed!\n2", "Indexed!\n3"], + ["Indexed!\n1\n", "Indexed!\n2\n", "Indexed!\n3\n"], ) # full-form highlighting options @@ -1650,7 +1650,7 @@ def test_boost(self): @unittest.skipIf( - parse_version(pysolr.__version__) < parse_version("3.1.1"), + Version(pysolr.__version__) < Version("3.1.1"), "content extraction requires pysolr > 3.1.1", ) class LiveSolrContentExtractionTestCase(TestCase): diff --git a/test_haystack/solr_tests/test_solr_management_commands.py b/test_haystack/solr_tests/test_solr_management_commands.py index 6c6a537e0..419d21b6d 100644 --- a/test_haystack/solr_tests/test_solr_management_commands.py +++ b/test_haystack/solr_tests/test_solr_management_commands.py @@ -1,5 +1,7 @@ import datetime import os +import shutil +import tempfile from io import StringIO from tempfile import mkdtemp from unittest.mock import patch @@ -202,7 +204,6 @@ def test_multiprocessing(self): self.assertEqual(self.solr.search("*:*").hits, 0) def test_build_schema_wrong_backend(self): - settings.HAYSTACK_CONNECTIONS["whoosh"] = { "ENGINE": "haystack.backends.whoosh_backend.WhooshEngine", "PATH": mkdtemp(prefix="dummy-path-"), @@ -214,12 +215,14 @@ def test_build_schema_wrong_backend(self): ) def test_build_schema(self): - # Stow. oldhdf = constants.DOCUMENT_FIELD oldui = connections["solr"].get_unified_index() oldurl = settings.HAYSTACK_CONNECTIONS["solr"]["URL"] + conf_dir = tempfile.mkdtemp() + with open(os.path.join(conf_dir, "managed-schema"), "w+") as fp: + pass try: needle = "Th3S3cr3tK3y" constants.DOCUMENT_FIELD = ( @@ -236,10 +239,6 @@ def test_build_schema(self): rendered_file = StringIO() - script_dir = os.path.realpath(os.path.dirname(__file__)) - conf_dir = os.path.join( - script_dir, "server", "solr", "server", "solr", "mgmnt", "conf" - ) schema_file = os.path.join(conf_dir, "schema.xml") solrconfig_file = os.path.join(conf_dir, "solrconfig.xml") @@ -263,16 +262,23 @@ def test_build_schema(self): os.path.isfile(os.path.join(conf_dir, "managed-schema.old")) ) - call_command("build_solr_schema", using="solr", reload_core=True) + with patch( + "haystack.management.commands.build_solr_schema.requests.get" + ) as mock_request: + call_command("build_solr_schema", using="solr", reload_core=True) - os.rename(schema_file, "%s.bak" % schema_file) - self.assertRaises( - CommandError, - call_command, - "build_solr_schema", - using="solr", - reload_core=True, - ) + with patch( + "haystack.management.commands.build_solr_schema.requests.get" + ) as mock_request: + mock_request.return_value.ok = False + + self.assertRaises( + CommandError, + call_command, + "build_solr_schema", + using="solr", + reload_core=True, + ) call_command("build_solr_schema", using="solr", filename=schema_file) with open(schema_file) as s: @@ -282,6 +288,7 @@ def test_build_schema(self): constants.DOCUMENT_FIELD = oldhdf connections["solr"]._index = oldui settings.HAYSTACK_CONNECTIONS["solr"]["URL"] = oldurl + shutil.rmtree(conf_dir, ignore_errors=True) class AppModelManagementCommandTestCase(TestCase): diff --git a/test_haystack/spatial/__init__.py b/test_haystack/spatial/__init__.py index 02a7dd78a..0cd29ea56 100644 --- a/test_haystack/spatial/__init__.py +++ b/test_haystack/spatial/__init__.py @@ -1,5 +1,12 @@ +import os + from ..utils import check_solr -def setup(): +def load_tests(loader, standard_tests, pattern): check_solr() + package_tests = loader.discover( + start_dir=os.path.dirname(__file__), pattern=pattern + ) + standard_tests.addTests(package_tests) + return standard_tests diff --git a/test_haystack/spatial/test_spatial.py b/test_haystack/spatial/test_spatial.py index 8218f9bf8..6d0fbc12a 100644 --- a/test_haystack/spatial/test_spatial.py +++ b/test_haystack/spatial/test_spatial.py @@ -106,6 +106,7 @@ def setUp(self): super().setUp() self.ui = connections[self.using].get_unified_index() + self.ui.reset() self.checkindex = self.ui.get_index(Checkin) self.checkindex.reindex(using=self.using) self.sqs = SearchQuerySet().using(self.using) diff --git a/test_haystack/test_app_using_appconfig/__init__.py b/test_haystack/test_app_using_appconfig/__init__.py index 30a0d2351..e69de29bb 100644 --- a/test_haystack/test_app_using_appconfig/__init__.py +++ b/test_haystack/test_app_using_appconfig/__init__.py @@ -1 +0,0 @@ -default_app_config = "test_app_using_appconfig.apps.SimpleTestAppConfig" diff --git a/test_haystack/test_app_using_appconfig/migrations/0001_initial.py b/test_haystack/test_app_using_appconfig/migrations/0001_initial.py index 1f9b7051e..309b49009 100644 --- a/test_haystack/test_app_using_appconfig/migrations/0001_initial.py +++ b/test_haystack/test_app_using_appconfig/migrations/0001_initial.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [] operations = [ diff --git a/test_haystack/test_django_config_detection.py b/test_haystack/test_django_config_detection.py index 31241a48f..0c3827882 100644 --- a/test_haystack/test_django_config_detection.py +++ b/test_haystack/test_django_config_detection.py @@ -1,4 +1,5 @@ """""" + import unittest import django diff --git a/test_haystack/test_indexes.py b/test_haystack/test_indexes.py index 19481ea51..6e6ee2d2d 100644 --- a/test_haystack/test_indexes.py +++ b/test_haystack/test_indexes.py @@ -687,8 +687,7 @@ class Meta: def get_index_fieldname(self, f): if f.name == "author": return "author_bar" - else: - return f.name + return f.name class YetAnotherBasicModelSearchIndex(indexes.ModelSearchIndex, indexes.Indexable): diff --git a/test_haystack/test_management_commands.py b/test_haystack/test_management_commands.py index 5d55de3a1..b66faf38f 100644 --- a/test_haystack/test_management_commands.py +++ b/test_haystack/test_management_commands.py @@ -77,8 +77,8 @@ def test_rebuild_index(self, mock_handle_clear, mock_handle_update): self.assertTrue(mock_handle_clear.called) self.assertTrue(mock_handle_update.called) - @patch("haystack.management.commands.update_index.Command.handle") - @patch("haystack.management.commands.clear_index.Command.handle") + @patch("haystack.management.commands.update_index.Command.handle", return_value="") + @patch("haystack.management.commands.clear_index.Command.handle", return_value="") def test_rebuild_index_nocommit(self, *mocks): call_command("rebuild_index", interactive=False, commit=False) @@ -92,7 +92,7 @@ def test_rebuild_index_nocommit(self, *mocks): @patch("haystack.management.commands.clear_index.Command.handle", return_value="") @patch("haystack.management.commands.update_index.Command.handle", return_value="") - def test_rebuild_index_nocommit(self, update_mock, clear_mock): + def test_rebuild_index_nocommit_two(self, update_mock, clear_mock): """ Confirm that command-line option parsing produces the same results as using call_command() directly, mostly as a sanity check for the logic in rebuild_index which combines the option_lists for its diff --git a/test_haystack/test_managers.py b/test_haystack/test_managers.py index 3784217cd..cc600752e 100644 --- a/test_haystack/test_managers.py +++ b/test_haystack/test_managers.py @@ -242,11 +242,11 @@ def spelling_suggestion(self): def test_values(self): sqs = self.search_index.objects.auto_query("test").values("id") - self.assert_(isinstance(sqs, ValuesSearchQuerySet)) + self.assertIsInstance(sqs, ValuesSearchQuerySet) def test_valueslist(self): sqs = self.search_index.objects.auto_query("test").values_list("id") - self.assert_(isinstance(sqs, ValuesListSearchQuerySet)) + self.assertIsInstance(sqs, ValuesListSearchQuerySet) class CustomManagerTestCase(TestCase): diff --git a/test_haystack/test_query.py b/test_haystack/test_query.py index ffe35c19a..c66d38427 100644 --- a/test_haystack/test_query.py +++ b/test_haystack/test_query.py @@ -95,6 +95,12 @@ def test_simple_nesting(self): class BaseSearchQueryTestCase(TestCase): fixtures = ["base_data.json", "bulk_data.json"] + @classmethod + def setUpClass(cls): + for connection in connections.all(): + connection.get_unified_index().reset() + super().setUpClass() + def setUp(self): super().setUp() self.bsq = BaseSearchQuery() @@ -442,7 +448,7 @@ def test_len(self): def test_repr(self): reset_search_queries() self.assertEqual(len(connections["default"].queries), 0) - self.assertRegexpMatches( + self.assertRegex( repr(self.msqs), r"^, using=None>$", @@ -967,18 +973,18 @@ def test_or_and(self): class ValuesQuerySetTestCase(SearchQuerySetTestCase): def test_values_sqs(self): sqs = self.msqs.auto_query("test").values("id") - self.assert_(isinstance(sqs, ValuesSearchQuerySet)) + self.assertIsInstance(sqs, ValuesSearchQuerySet) # We'll do a basic test to confirm that slicing works as expected: - self.assert_(isinstance(sqs[0], dict)) - self.assert_(isinstance(sqs[0:5][0], dict)) + self.assertIsInstance(sqs[0], dict) + self.assertIsInstance(sqs[0:5][0], dict) def test_valueslist_sqs(self): sqs = self.msqs.auto_query("test").values_list("id") - self.assert_(isinstance(sqs, ValuesListSearchQuerySet)) - self.assert_(isinstance(sqs[0], (list, tuple))) - self.assert_(isinstance(sqs[0:1][0], (list, tuple))) + self.assertIsInstance(sqs, ValuesListSearchQuerySet) + self.assertIsInstance(sqs[0], (list, tuple)) + self.assertIsInstance(sqs[0:1][0], (list, tuple)) self.assertRaises( TypeError, @@ -989,12 +995,12 @@ def test_valueslist_sqs(self): ) flat_sqs = self.msqs.auto_query("test").values_list("id", flat=True) - self.assert_(isinstance(sqs, ValuesListSearchQuerySet)) + self.assertIsInstance(sqs, ValuesListSearchQuerySet) # Note that this will actually be None because a mocked sqs lacks # anything else: - self.assert_(flat_sqs[0] is None) - self.assert_(flat_sqs[0:1][0] is None) + self.assertIsNone(flat_sqs[0]) + self.assertIsNone(flat_sqs[0:1][0]) class EmptySearchQuerySetTestCase(TestCase): diff --git a/test_haystack/whoosh_tests/test_forms.py b/test_haystack/whoosh_tests/test_forms.py index 204d14f46..64be222fc 100644 --- a/test_haystack/whoosh_tests/test_forms.py +++ b/test_haystack/whoosh_tests/test_forms.py @@ -1,4 +1,5 @@ """Tests for Whoosh spelling suggestions""" + from django.conf import settings from django.http import HttpRequest diff --git a/test_haystack/whoosh_tests/test_whoosh_backend.py b/test_haystack/whoosh_tests/test_whoosh_backend.py index fd5f56e14..5de276b5e 100644 --- a/test_haystack/whoosh_tests/test_whoosh_backend.py +++ b/test_haystack/whoosh_tests/test_whoosh_backend.py @@ -1,12 +1,11 @@ import os import unittest -from datetime import timedelta +from datetime import date, datetime, timedelta from decimal import Decimal from django.conf import settings from django.test import TestCase from django.test.utils import override_settings -from django.utils.datetime_safe import date, datetime from whoosh.analysis import SpaceSeparatedTokenizer, SubstitutionFilter from whoosh.fields import BOOLEAN, DATETIME, KEYWORD, NUMERIC, TEXT from whoosh.qparser import QueryParser @@ -115,6 +114,7 @@ def get_model(self): return MockModel +@override_settings(USE_TZ=False) class WhooshSearchBackendTestCase(WhooshTestCase): fixtures = ["bulk_data.json"] diff --git a/tox.ini b/tox.ini index d4ec71035..d5a436091 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,37 @@ [tox] envlist = docs - py{36,37,38,39,310,py}-django{2.2,3.0,3.1,3.2,4.0}-es{1.x,2.x,5.x,7.x} + py{38,39,310,311,312}-django{3.2,4.2,5.0}-es7.x +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + +[gh-actions:env] +DJANGO = + 3.2: django3.2 + 4.2: django4.2 + 5.0: django5.0 [testenv] commands = python test_haystack/solr_tests/server/wait-for-solr - python {toxinidir}/setup.py test + coverage run {toxinidir}/test_haystack/run_tests.py deps = + pysolr>=3.7.0 + whoosh>=2.5.4,<3.0 + python-dateutil + geopy==2.0.0 + coverage requests - django2.2: Django>=2.2,<3.0 - django3.0: Django>=3.0,<3.1 - django3.1: Django>=3.1,<3.2 + setuptools; python_version >= "3.12" # Can be removed on pysolr >= v3.10 django3.2: Django>=3.2,<3.3 - django4.0: Django>=4.0,<4.1 + django4.2: Django>=4.2,<4.3 + django5.0: Django>=5.0,<5.1 es1.x: elasticsearch>=1,<2 es2.x: elasticsearch>=2,<3 es5.x: elasticsearch>=5,<6