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..5f6e6378b 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.15.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.2
+ 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..e573494f2 100644
--- a/README.rst
+++ b/README.rst
@@ -59,9 +59,19 @@ Requirements
Haystack has a relatively easily-met set of requirements.
-* Python 3.6+
+* A supported version of Python: https://devguide.python.org/versions/#supported-versions
* A supported version of Django: https://www.djangoproject.com/download/#supported-versions
Additionally, each backend has its own requirements. You should refer to
https://django-haystack.readthedocs.io/en/latest/installing_search_engines.html for more
details.
+
+Experimental support for Django v5.0
+====================================
+
+The current release on PyPI_ does not yet support Django v5.0.
+
+.. _PyPI: https://pypi.org/project/django-haystack/
+
+To run on Django v5.0, please install by using:
+``pip install git+https://github.com/django-haystack/django-haystack.git``
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/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..161a9038a 100644
--- a/haystack/backends/elasticsearch7_backend.py
+++ b/haystack/backends/elasticsearch7_backend.py
@@ -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 = {}
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