Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,15 @@ jobs:
uses: codecov/codecov-action@v5
with:
name: Python ${{ matrix.python-version }}

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/ruff-action@v3
with:
version: "latest"
- name: Ruff lint
run: ruff check --fix
- name: Ruff format
run: ruff format
Comment on lines +50 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend adding an action for pre-commit hooks, instead of a ruff-only config.

12 changes: 7 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ repos:
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
Expand All @@ -20,3 +15,10 @@ repos:
- id: fix-byte-order-marker
- id: mixed-line-ending
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
hooks:
- id: ruff
args: [ --fix ]
- id: ruff-format
2 changes: 0 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ VENV = /tmp/django-hosts-env
export PATH := $(VENV)/bin:$(PATH)

test:
flake8 django_hosts
pytest


clean:
make -C docs clean
rm -rf $(VENV)
Expand Down
22 changes: 11 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ at ``api.example.com`` and ``beta.example.com``, add the following to a

.. code-block:: python

from django_hosts import patterns, host
from django_hosts import host, patterns

host_patterns = patterns('path.to',
host(r'api', 'api.urls', name='api'),
host(r'beta', 'beta.urls', name='beta'),
host_patterns = patterns("path.to",
host(r"api", "api.urls", name="api"),
host(r"beta", "beta.urls", name="beta"),
)

This causes requests to ``{api,beta}.example.com`` to be routed to their
Expand All @@ -51,10 +51,10 @@ and ``bar.example.com`` to the same URLconf.

.. code-block:: python

from django_hosts import patterns, host
from django_hosts import host, patterns

host_patterns = patterns('',
host(r'(foo|bar)', 'path.to.urls', name='foo-or-bar'),
host_patterns = patterns("",
host(r"(foo|bar)", "path.to.urls", name="foo-or-bar"),
)

.. note:
Expand Down Expand Up @@ -83,12 +83,12 @@ You can find the full docs here: `django-hosts.rtfd.org`_

Then configure your Django site to use the app:

#. Add ``'django_hosts'`` to your ``INSTALLED_APPS`` setting.
#. Add ``"django_hosts"`` to your ``INSTALLED_APPS`` setting.

#. Add ``'django_hosts.middleware.HostsRequestMiddleware'`` to the
#. Add ``"django_hosts.middleware.HostsRequestMiddleware"`` to the
**beginning** of your ``MIDDLEWARE`` setting.

#. Add ``'django_hosts.middleware.HostsResponseMiddleware'`` to the **end** of
#. Add ``"django_hosts.middleware.HostsResponseMiddleware"`` to the **end** of
your ``MIDDLEWARE`` setting.

#. Create a new module containing your default host patterns,
Expand All @@ -99,7 +99,7 @@ Then configure your Django site to use the app:

.. code-block:: python

ROOT_HOSTCONF = 'mysite.hosts'
ROOT_HOSTCONF = "mysite.hosts"

#. Set the ``DEFAULT_HOST`` setting to the **name** of the host pattern you
want to refer to as the default pattern. It'll be used if no other
Expand Down
14 changes: 6 additions & 8 deletions django_hosts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# flake8: noqa
import importlib.metadata

try: # pragma: no cover
from django_hosts.defaults import patterns, host
from django_hosts.resolvers import (reverse, reverse_lazy,
reverse_host, reverse_host_lazy)
except ImportError: # pragma: no cover
try:
from django_hosts.defaults import host, patterns
from django_hosts.resolvers import reverse, reverse_host, reverse_host_lazy, reverse_lazy
except ImportError:
pass

__version__ = importlib.metadata.version('django-hosts')
__author__ = 'Jazzband members (https://jazzband.co/)'
__version__ = importlib.metadata.version("django-hosts")
__author__ = "Jazzband members (https://jazzband.co/)"
4 changes: 2 additions & 2 deletions django_hosts/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@


class HostsConfig(AppConfig): # pragma: no cover
name = 'django_hosts'
verbose_name = _('Hosts')
name = "django_hosts"
verbose_name = _("Hosts")

def ready(self):
checks.register(check_root_hostconf)
Expand Down
39 changes: 21 additions & 18 deletions django_hosts/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,40 @@

from .resolvers import reverse_host

HOST_SITE_TIMEOUT = getattr(settings, 'HOST_SITE_TIMEOUT', 3600)
HOST_SITE_TIMEOUT = getattr(settings, "HOST_SITE_TIMEOUT", 3600)


class LazySite(LazyObject):

def __init__(self, request, *args, **kwargs):
super().__init__()
self.__dict__.update({
'name': request.host.name,
'args': args,
'kwargs': kwargs,
})
self.__dict__.update(
{
"name": request.host.name,
"args": args,
"kwargs": kwargs,
}
)

def _setup(self):
host = reverse_host(self.name, args=self.args, kwargs=self.kwargs)
from django.contrib.sites.models import Site

site = get_object_or_404(Site, domain__iexact=host)
self._wrapped = site


class CachedLazySite(LazySite):

def _setup(self):
host = reverse_host(self.name, args=self.args, kwargs=self.kwargs)
cache_key = "hosts:%s" % host
cache_key = f"hosts:{host}"
from django.core.cache import cache

site = cache.get(cache_key, None)
if site is not None:
self._wrapped = site
return
from django.contrib.sites.models import Site

site = get_object_or_404(Site, domain__iexact=host)
cache.set(cache_key, site, HOST_SITE_TIMEOUT)
self._wrapped = site
Expand All @@ -61,24 +64,24 @@ def host_site(request, *args, **kwargs):
For example, imagine a host conf with a username parameter::

from django.conf import settings
from django_hosts import patterns, host
from django_hosts import host, patterns

settings.PARENT_HOST = 'example.com'
settings.PARENT_HOST = "example.com"

host_patterns = patterns('',
host(r'www', settings.ROOT_URLCONF, name='www'),
host(r'(?P<username>\w+)', 'path.to.custom_urls',
callback='django_hosts.callbacks.host_site',
name='user-sites'),
host_patterns = patterns("",
host(r"www", settings.ROOT_URLCONF, name="www"),
host(r"(?P<username>\w+)", "path.to.custom_urls",
callback="django_hosts.callbacks.host_site",
name="user-sites"),
)

When requesting this website with the host ``jezdez.example.com``,
the callback will act as if you'd do::

request.site = Site.objects.get(domain__iexact='jezdez.example.com')
request.site = Site.objects.get(domain__iexact="jezdez.example.com")

..since the result of calling :func:`~django_hosts.resolvers.reverse_host`
with the username ``'jezdez'`` is ``'jezdez.example.com'``.
with the username ``"jezdez"`` is ``"jezdez.example.com"``.

Later, in your views, you can nicely refer to the current site
as ``request.site`` for further site-specific functionality.
Expand Down
11 changes: 5 additions & 6 deletions django_hosts/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@
E001 = checks.Error(
"Missing 'DEFAULT_HOST' setting.",
hint="Has to be the name of the default host pattern.",
id='django_hosts.E001',
id="django_hosts.E001",
)

E002 = checks.Error(
"Missing 'ROOT_HOSTCONF' setting.",
hint="Has to be the dotted Python import path of "
"the module containing your host patterns.",
id='django_hosts.E002',
hint="Has to be the dotted Python import path of the module containing your host patterns.",
id="django_hosts.E002",
)


def check_default_host(app_configs, **kwargs): # pragma: no cover
return [] if getattr(settings, 'DEFAULT_HOST', False) else [E001]
return [] if getattr(settings, "DEFAULT_HOST", False) else [E001]


def check_root_hostconf(app_configs, **kwargs): # pragma: no cover
return [] if getattr(settings, 'ROOT_HOSTCONF', False) else [E002]
return [] if getattr(settings, "ROOT_HOSTCONF", False) else [E002]
63 changes: 32 additions & 31 deletions django_hosts/defaults.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"""
When defining hostconfs you need to use the ``patterns`` and ``host`` helpers
"""

import re

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.urls import (
get_callable as actual_get_callable, get_mod_func,
get_callable as actual_get_callable,
get_mod_func,
)
from django.utils.encoding import smart_str
from django.utils.functional import cached_property

from .utils import normalize_scheme, normalize_port
from .utils import normalize_port, normalize_scheme


def get_callable(lookup_view):
Expand All @@ -23,7 +26,7 @@ def get_callable(lookup_view):
try:
return actual_get_callable(lookup_view)
except ViewDoesNotExist as exc:
raise ImproperlyConfigured(exc.args[0].replace('View', 'Callable'))
raise ImproperlyConfigured(exc.args[0].replace("View", "Callable")) from exc


def patterns(prefix, *args):
Expand All @@ -32,9 +35,9 @@ def patterns(prefix, *args):

from django_hosts import patterns

host_patterns = patterns('path.to',
(r'www', 'urls.default', 'default'),
(r'api', 'urls.api', 'api'),
host_patterns = patterns("path.to",
(r"www", "urls.default", "default"),
(r"api", "urls.api", "api"),
)

:param prefix: the URLconf prefix to pass to the host object
Expand All @@ -45,12 +48,12 @@ def patterns(prefix, *args):
hosts = []
for arg in args:
if isinstance(arg, (list, tuple)):
arg = host(prefix=prefix, *arg)
arg = host(prefix=prefix, *arg) # noqa: B026
else:
arg.add_prefix(prefix)
name = arg.name
if name in [h.name for h in hosts]:
raise ImproperlyConfigured("Duplicate host name: %s" % name)
raise ImproperlyConfigured(f"Duplicate host name: {name}")
hosts.append(arg)
return hosts

Expand All @@ -60,12 +63,12 @@ class host:
The host object used in host conf together with the
:func:`django_hosts.defaults.patterns` function, e.g.::

from django_hosts import patterns, host
from django_hosts import host, patterns

host_patterns = patterns('path.to',
host(r'www', 'urls.default', name='default'),
host(r'api', 'urls.api', name='api'),
host(r'admin', 'urls.admin', name='admin', scheme='https://'),
host_patterns = patterns("path.to",
host(r"www", "urls.default", name="default"),
host(r"api", "urls.api", name="api"),
host(r"admin", "urls.admin", name="admin", scheme="https://"),
)

:param regex: a regular expression to be used to match the request's
Expand All @@ -86,17 +89,17 @@ class host:
:attr:`~django.conf.settings.HOST_PORT`.
:type scheme: str
"""
def __init__(self, regex, urlconf, name, callback=None, prefix='',
scheme=None, port=None):

def __init__(self, regex, urlconf, name, callback=None, prefix="", scheme=None, port=None):
"""
Compile hosts. We add a literal fullstop to the end of every
pattern to avoid rather unwieldy escaping in every definition.
The pattern is also suffixed by the PARENT_HOST setting if it exists.
"""
self.regex = regex
parent_host = getattr(settings, 'PARENT_HOST', '').lstrip('.')
suffix = r'\.' + parent_host if parent_host else ''
self.compiled_regex = re.compile(fr'{regex}{suffix}(\.|:|$)')
parent_host = getattr(settings, "PARENT_HOST", "").lstrip(".")
suffix = r"\." + parent_host if parent_host else ""
self.compiled_regex = re.compile(rf"{regex}{suffix}(\.|:|$)")
self.urlconf = urlconf
self.name = name
self._scheme = scheme
Expand All @@ -108,20 +111,20 @@ def __init__(self, regex, urlconf, name, callback=None, prefix='',
self.add_prefix(prefix)

def __repr__(self):
return smart_str('<%s %s: regex=%r urlconf=%r scheme=%r port=%r>' %
(self.__class__.__name__, self.name, self.regex,
self.urlconf, self.scheme, self.port))
return smart_str(
f'<{self.__class__.__name__} {self.name}: regex="{self.regex}" urlconf="{self.urlconf}" scheme="{self.scheme}" port="{self.port}">' # noqa: E501
)

@cached_property
def scheme(self):
if self._scheme is None:
self._scheme = getattr(settings, 'HOST_SCHEME', '//')
self._scheme = getattr(settings, "HOST_SCHEME", "//")
return normalize_scheme(self._scheme)

@cached_property
def port(self):
if self._port is None:
self._port = getattr(settings, 'HOST_PORT', '')
self._port = getattr(settings, "HOST_PORT", "")
return normalize_port(self._port)

@property
Expand All @@ -134,19 +137,17 @@ def callback(self):
self._callback = get_callable(self._callback_str)
except ImportError as exc:
mod_name, _ = get_mod_func(self._callback_str)
raise ImproperlyConfigured("Could not import '%s'. "
"Error was: %s" %
(mod_name, str(exc)))
raise ImproperlyConfigured(f"Could not import '{mod_name}'. Error was: {str(exc)}") from exc
except AttributeError as exc:
mod_name, func_name = get_mod_func(self._callback_str)
raise ImproperlyConfigured("Tried importing '%s' from module "
"'%s' but failed. Error was: %s" %
(func_name, mod_name, str(exc)))
raise ImproperlyConfigured(
f"Tried importing '{func_name}' from module '{mod_name}' but failed. Error was: {str(exc)}"
) from exc
return self._callback

def add_prefix(self, prefix=''):
def add_prefix(self, prefix=""):
"""
Adds the prefix string to a string-based urlconf.
"""
if prefix:
self.urlconf = prefix.rstrip('.') + '.' + self.urlconf
self.urlconf = prefix.rstrip(".") + "." + self.urlconf
Loading