diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0bf3b94..f89e0e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,14 +11,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b6ff63..2827a1b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,23 +9,23 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Get pip cache dir id: pip-cache run: | - echo "::set-output name=dir::$(pip cache dir)" + echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT - name: Cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.pip-cache.outputs.dir }} key: @@ -43,6 +43,6 @@ jobs: tox -v - name: Upload coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 with: name: Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index 59c024f..7486424 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,16 @@ *.pyc -docs/_build .tox +.venv/ +venv/ +.eggs/ *.egg-info *.egg .coverage coverage.xml reports/ build/ +docs/_build dist/ .cache/ -.eggs/ +.idea/ +.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a62c209..67bb0c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1 +1,22 @@ -repos: [] \ No newline at end of file +repos: +- repo: https://github.com/adamchainz/django-upgrade + rev: 1.23.1 + hooks: + - id: django-upgrade + args: [--target-version, "4.2"] +- repo: https://github.com/asottile/pyupgrade + rev: v3.19.1 + hooks: + - id: pyupgrade + args: [--py39-plus] + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-executables-have-shebangs + - id: check-illegal-windows-names + - id: check-merge-conflict + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: trailing-whitespace diff --git a/MANIFEST.in b/MANIFEST.in index f26f92b..08f0958 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include AUTHORS include README.rst -recursive-include docs * \ No newline at end of file +recursive-include docs * diff --git a/django_hosts/callbacks.py b/django_hosts/callbacks.py index 0e73774..384712c 100644 --- a/django_hosts/callbacks.py +++ b/django_hosts/callbacks.py @@ -10,7 +10,7 @@ class LazySite(LazyObject): def __init__(self, request, *args, **kwargs): - super(LazySite, self).__init__() + super().__init__() self.__dict__.update({ 'name': request.host.name, 'args': args, diff --git a/django_hosts/defaults.py b/django_hosts/defaults.py index 424a9cc..31cab37 100644 --- a/django_hosts/defaults.py +++ b/django_hosts/defaults.py @@ -55,7 +55,7 @@ def patterns(prefix, *args): return hosts -class host(object): +class host: """ The host object used in host conf together with the :func:`django_hosts.defaults.patterns` function, e.g.:: @@ -96,7 +96,7 @@ def __init__(self, regex, urlconf, name, callback=None, prefix='', self.regex = regex parent_host = getattr(settings, 'PARENT_HOST', '').lstrip('.') suffix = r'\.' + parent_host if parent_host else '' - self.compiled_regex = re.compile(r'%s%s(\.|:|$)' % (regex, suffix)) + self.compiled_regex = re.compile(fr'{regex}{suffix}(\.|:|$)') self.urlconf = urlconf self.name = name self._scheme = scheme diff --git a/django_hosts/managers.py b/django_hosts/managers.py index 0407fc2..6708b11 100644 --- a/django_hosts/managers.py +++ b/django_hosts/managers.py @@ -34,7 +34,7 @@ def home_page(request): """ def __init__(self, field_name=None, select_related=True): - super(HostSiteManager, self).__init__() + super().__init__() self._field_name = field_name self._select_related = select_related self._depth = 1 @@ -81,7 +81,7 @@ def get_queryset(self, site_id=None): site_id = settings.SITE_ID if not self._is_validated: self._validate_field_name() - qs = super(HostSiteManager, self).get_queryset() + qs = super().get_queryset() return qs.filter(**{'%s__id__exact' % self._field_name: site_id}) def by_id(self, site_id=None): diff --git a/django_hosts/middleware.py b/django_hosts/middleware.py index 0673270..5406c97 100644 --- a/django_hosts/middleware.py +++ b/django_hosts/middleware.py @@ -14,9 +14,7 @@ class HostsBaseMiddleware(MiddlewareMixin): new_hosts_middleware = 'django_hosts.middleware.HostsRequestMiddleware' toolbar_middleware = 'debug_toolbar.middleware.DebugToolbarMiddleware' - # TODO: when support for Django 3.2 is removed, replace with: - # def __init__(self, get_response): - def __init__(self, get_response=None): + def __init__(self, get_response): super().__init__(get_response) self.current_urlconf = None self.host_patterns = get_host_patterns() diff --git a/django_hosts/resolvers.py b/django_hosts/resolvers.py index e27bdc7..78a9102 100644 --- a/django_hosts/resolvers.py +++ b/django_hosts/resolvers.py @@ -19,7 +19,7 @@ from .utils import normalize_scheme, normalize_port -@lru_cache() +@lru_cache def get_hostconf(): try: return settings.ROOT_HOSTCONF @@ -27,14 +27,14 @@ def get_hostconf(): raise ImproperlyConfigured("Missing ROOT_HOSTCONF setting") -@lru_cache() +@lru_cache def get_hostconf_module(hostconf=None): if hostconf is None: hostconf = get_hostconf() return import_module(hostconf) -@lru_cache() +@lru_cache def get_host(name=None): if name is None: try: @@ -47,7 +47,7 @@ def get_host(name=None): raise NoReverseMatch("No host called '%s' exists" % name) -@lru_cache() +@lru_cache def get_host_patterns(): hostconf = get_hostconf() module = get_hostconf_module(hostconf) @@ -115,7 +115,7 @@ def reverse_host(host, args=None, kwargs=None): if parent_host: # only add the parent host when needed (aka www-less domain) if candidate and candidate != parent_host: - candidate = '%s.%s' % (candidate, parent_host) + candidate = f'{candidate}.{parent_host}' else: candidate = parent_host return candidate @@ -189,7 +189,7 @@ def reverse(viewname, args=None, kwargs=None, prefix=None, current_app=None, else: port = normalize_port(port) - return iri_to_uri('%s%s%s%s' % (scheme, hostname, port, path)) + return iri_to_uri(f'{scheme}{hostname}{port}{path}') #: The lazy version of the :func:`~django_hosts.resolvers.reverse` diff --git a/django_hosts/templatetags/hosts.py b/django_hosts/templatetags/hosts.py index 7549ea4..0bbcd4b 100644 --- a/django_hosts/templatetags/hosts.py +++ b/django_hosts/templatetags/hosts.py @@ -24,7 +24,7 @@ def __init__(self, *args, **kwargs): self.host_kwargs = kwargs.pop('host_kwargs') self.scheme = kwargs.pop('scheme') self.port = kwargs.pop('port') - super(HostURLNode, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def maybe_resolve(self, var, context): """ @@ -41,7 +41,7 @@ def render(self, context): current_urlconf = get_urlconf() try: set_urlconf(host.urlconf) - path = super(HostURLNode, self).render(context) + path = super().render(context) if self.asvar: path = context[self.asvar] finally: @@ -49,9 +49,10 @@ def render(self, context): host_args = [self.maybe_resolve(x, context) for x in self.host_args] - host_kwargs = dict((smart_str(k, 'ascii'), - self.maybe_resolve(v, context)) - for k, v in self.host_kwargs.items()) + host_kwargs = { + smart_str(k, 'ascii'): self.maybe_resolve(v, context) + for k, v in self.host_kwargs.items() + } if self.scheme: scheme = normalize_scheme(self.maybe_resolve(self.scheme, context)) @@ -65,7 +66,7 @@ def render(self, context): hostname = reverse_host(host, args=host_args, kwargs=host_kwargs) - uri = iri_to_uri('%s%s%s%s' % (scheme, hostname, port, path)) + uri = iri_to_uri(f'{scheme}{hostname}{port}{path}') if self.asvar: context[self.asvar] = uri diff --git a/django_hosts/utils.py b/django_hosts/utils.py index 76f9701..e9fb256 100644 --- a/django_hosts/utils.py +++ b/django_hosts/utils.py @@ -1,4 +1,3 @@ - def normalize_scheme(scheme=None, default='//'): if scheme is None: scheme = default diff --git a/docs/changelog.rst b/docs/changelog.rst index ae945fe..ce899c6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,9 +4,19 @@ Changelog X.Y (unreleased) ---------------- -- **BACKWARD-INCOMPATIBLE** Dropped support Django < 4.2. +- **BACKWARD-INCOMPATIBLE** Dropped support for Python 3.8. -- Confirmed support for Django 4.2 and 5.0 (no code changes were required). +- **BACKWARD-INCOMPATIBLE** Dropped support for Django < 4.2 and Django 5.0 + (support for 5.0 was added to an unreleased version). + +- **BACKWARD-INCOMPATIBLE** Removed the ``None`` default value of the + ``get_response`` parameter of ``HostsBaseMiddleware()``, after the argument + was made required with Django 4.0+. + +- Confirmed support for Python 3.13. + +- Confirmed support for Django 4.2, 5.1 and 5.2 + (no functional code changes were required). 6.0 (2023-10-27) ---------------- diff --git a/docs/conf.py b/docs/conf.py index fc6460a..8ed1496 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # django-hosts documentation build configuration file, created by # sphinx-quickstart on Mon Sep 26 16:39:46 2011. # @@ -43,8 +41,8 @@ master_doc = 'index' # General information about the project. -project = u'django-hosts' -copyright = u'2015-2016, Jazzband members (https://jazzband.co/)' +project = 'django-hosts' +copyright = '2015-2016, Jazzband members (https://jazzband.co/)' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -185,8 +183,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-hosts.tex', u'django-hosts Documentation', - u'Jannis Leidel and contributors', 'manual'), + ('index', 'django-hosts.tex', 'django-hosts Documentation', + 'Jannis Leidel and contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -218,8 +216,8 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-hosts', u'django-hosts Documentation', - [u'Jannis Leidel and contributors'], 1) + ('index', 'django-hosts', 'django-hosts Documentation', + ['Jannis Leidel and contributors'], 1) ] # Example configuration for intersphinx: refer to the Python standard library. diff --git a/docs/faq.rst b/docs/faq.rst index 4a2ccfe..9613ff4 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -33,4 +33,3 @@ depending on the host that should handle the call .. code-block:: python client.post(..., SERVER_NAME='api-server.something') - diff --git a/setup.py b/setup.py index e8ee73e..fefa67a 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ def read(*parts): 'Maps hostnames to URLconfs.', long_description=read('README.rst'), use_scm_version=True, - python_requires='>=3.8', + python_requires='>=3.9', setup_requires=['setuptools_scm'], url='https://django-hosts.readthedocs.io/', project_urls={ @@ -30,17 +30,18 @@ def read(*parts): 'Environment :: Web Environment', 'Framework :: Django', 'Framework :: Django :: 4.2', - 'Framework :: Django :: 5.0', + 'Framework :: Django :: 5.1', + 'Framework :: Django :: 5.2', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], ) diff --git a/tests/base.py b/tests/base.py index fefaa50..b90eeaf 100644 --- a/tests/base.py +++ b/tests/base.py @@ -6,6 +6,6 @@ class HostsTestCase(TestCase): def setUp(self): - super(HostsTestCase, self).setUp() + super().setUp() # Every test needs access to the request factory. self.factory = RequestFactory() diff --git a/tests/settings.py b/tests/settings.py index 0babb0e..b3cd59d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -25,4 +25,4 @@ SECRET_KEY = 'something-something' -USE_TZ = True \ No newline at end of file +USE_TZ = True diff --git a/tests/test_middleware.py b/tests/test_middleware.py index cbcfdd2..01ccc58 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -47,7 +47,7 @@ def test_wrong_default_hosts(self): ROOT_HOSTCONF='tests.hosts.simple', DEFAULT_HOST='www') def test_request_urlconf_module(self): - rf = RequestFactory(HTTP_HOST='other.example.com') + rf = RequestFactory(headers={'host': 'other.example.com'}) request = rf.get('/simple/') middleware = HostsRequestMiddleware(get_response_empty) middleware.process_request(request) @@ -59,7 +59,7 @@ def test_request_urlconf_module(self): PARENT_HOST='example.com', DEFAULT_HOST='root') def test_request_blank_urlconf_module(self): - rf = RequestFactory(HTTP_HOST='example.com') + rf = RequestFactory(headers={'host': 'example.com'}) request = rf.get('/') middleware = HostsRequestMiddleware(get_response_empty) middleware.process_request(request) @@ -70,7 +70,7 @@ def test_request_blank_urlconf_module(self): ROOT_HOSTCONF='tests.hosts.simple', DEFAULT_HOST='www') def test_response_urlconf_module(self): - rf = RequestFactory(HTTP_HOST='other.example.com') + rf = RequestFactory(headers={'host': 'other.example.com'}) request = rf.get('/simple/') middleware = HostsResponseMiddleware(get_response_empty) middleware.process_response(request, HttpResponse('test')) @@ -81,7 +81,7 @@ def test_response_urlconf_module(self): ROOT_HOSTCONF='tests.hosts.simple', DEFAULT_HOST='with_view_kwargs') def test_fallback_to_defaulthost(self): - rf = RequestFactory(HTTP_HOST='ss.example.com') + rf = RequestFactory(headers={'host': 'ss.example.com'}) request = rf.get('/template/test/') middleware = HostsRequestMiddleware(get_response_empty) middleware.process_request(request) @@ -124,7 +124,7 @@ async def test_asgi_request(self): 'django_hosts.middleware.HostsResponseMiddleware', ]) def test_fallback_with_evil_host(self): - response = self.client.get('/', HTTP_HOST='evil.com') + response = self.client.get('/', headers={'host': 'evil.com'}) self.assertEqual(response.status_code, 400) @override_settings( @@ -132,7 +132,7 @@ def test_fallback_with_evil_host(self): ROOT_HOSTCONF='tests.hosts.multiple', DEFAULT_HOST='multiple') def test_multiple_subdomains(self): - rf = RequestFactory(HTTP_HOST='spam.eggs.example.com') + rf = RequestFactory(headers={'host': 'spam.eggs.example.com'}) request = rf.get('/multiple/') middleware = HostsRequestMiddleware(get_response_empty) middleware.process_request(request) diff --git a/tests/test_sites.py b/tests/test_sites.py index 4c59420..916c04a 100644 --- a/tests/test_sites.py +++ b/tests/test_sites.py @@ -18,7 +18,7 @@ def get_response_empty(request): class SitesTests(HostsTestCase): def setUp(self): - super(SitesTests, self).setUp() + super().setUp() self.site1 = Site.objects.create(domain='wiki.site1', name='site1') self.site2 = Site.objects.create(domain='wiki.site2', name='site2') self.site3 = Site.objects.create(domain='wiki.site3', name='site3') @@ -43,7 +43,7 @@ def tearDown(self): ROOT_HOSTCONF='tests.hosts.simple', DEFAULT_HOST='www') def test_sites_callback(self): - rf = RequestFactory(HTTP_HOST='wiki.site1') + rf = RequestFactory(headers={'host': 'wiki.site1'}) request = rf.get('/simple/') middleware = HostsRequestMiddleware(get_response_empty) middleware.process_request(request) @@ -54,7 +54,7 @@ def test_sites_callback(self): ROOT_HOSTCONF='tests.hosts.simple', DEFAULT_HOST='www') def test_sites_cached_callback(self): - rf = RequestFactory(HTTP_HOST='admin.site4') + rf = RequestFactory(headers={'host': 'admin.site4'}) request = rf.get('/simple/') middleware = HostsRequestMiddleware(get_response_empty) middleware.process_request(request) @@ -75,7 +75,7 @@ def test_sites_cached_callback(self): ROOT_HOSTCONF='tests.hosts.simple', DEFAULT_HOST='www') def test_sites_callback_with_parent_host(self): - rf = RequestFactory(HTTP_HOST='wiki.site2') + rf = RequestFactory(headers={'host': 'wiki.site2'}) request = rf.get('/simple/') middleware = HostsRequestMiddleware(get_response_empty) middleware.process_request(request) @@ -86,7 +86,7 @@ def test_sites_callback_with_parent_host(self): ROOT_HOSTCONF='tests.hosts.simple', DEFAULT_HOST='www') def test_manager_simple(self): - rf = RequestFactory(HTTP_HOST='wiki.site2') + rf = RequestFactory(headers={'host': 'wiki.site2'}) request = rf.get('/simple/') middleware = HostsRequestMiddleware(get_response_empty) middleware.process_request(request) @@ -99,7 +99,7 @@ def test_manager_simple(self): ROOT_HOSTCONF='tests.hosts.simple', DEFAULT_HOST='www') def test_manager_missing_site(self): - rf = RequestFactory(HTTP_HOST='static') + rf = RequestFactory(headers={'host': 'static'}) request = rf.get('/simple/') middleware = HostsRequestMiddleware(get_response_empty) middleware.process_request(request) diff --git a/tests/urls/root.py b/tests/urls/root.py index 55ef769..637600f 100644 --- a/tests/urls/root.py +++ b/tests/urls/root.py @@ -1,3 +1 @@ - - urlpatterns = [] diff --git a/tox.ini b/tox.ini index be2e565..7b79ba7 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,9 @@ downloadcache = {distshare} args_are_paths = false envlist = - py{38,39,310,311,312}-dj42 - py{310,311,312}-dj50 - py{310,311,312}-djmain + py{39,310,311,312}-dj42 + py{310,311,312,313}-dj{51,52} + py{312,313}-djmain [testenv] usedevelop = true @@ -12,7 +12,8 @@ commands = make test allowlist_externals = make deps = dj42: Django>=4.2,<5.0 - dj50: Django>=5.0,<5.1 + dj51: Django>=5.1,<5.2 + dj52: Django>=5.2a1,<5.3 djmain: https://github.com/django/django/tarball/main coverage flake8 @@ -21,8 +22,8 @@ deps = [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313