"""This module contains the "main" abstract route class, that provides
:func:`BaseRoute.patterns`, yielding patterns made from the route
metadata and using the callback returned by implementations of the
abstract function :func:`BaseRoute.get_callback`.
.. note::
This route class is one the two (along with
:class:`django_crucrudile.routers.Router`) standard implementations
of the :class:`django_crucrudile.entities.Entity` abstract class.
"""
from itertools import product
from abc import abstractmethod
from django.conf.urls import url
from django_crucrudile.entities import Entity
from django_crucrudile.urlutils import URLBuilder
[docs]class BaseRoute(Entity):
"""Abstract class for a :class:`django_crucrudile.entities.Entity`
that URL patterns that point to its implementation of
:func:`get_callback`. Implements
:func:`django_crucrudile.entities.Entity.patterns` using
:func:`patterns`.
.. warning::
Abstract class ! Subclasses should define the
:func:`get_callback` function. See warning in :func:`__init__`.
The URL part and URL name must be either set on class, or given at
:func:`__init__`.
.. inheritance-diagram:: BaseRoute
"""
name = None
"""
:attribute name: URL name to use for this pattern (will be used
for the Django URL pattern name)
:type name: str
"""
url_part = None
"""
:attribute url_part: URL regex to use for the pattern (will be
used in the URL regexp)
:type url_part: str
"""
auto_url_part = True
"""
:attribute auto_url_part: Automatically set :attr:`url_part` to
:attr:`name` if none defined.
:type auto_url_part: bool
"""
[docs] def __init__(self,
name=None, url_part=None,
**kwargs):
"""Initialize Route, check that needed attributes/arguments are
defined.
Also sets ``self.redirect`` to the URL name (using
:func:`get_url_name`).
:argument name: See :attr:`name`
:argument url_part: See :attr:`url_part`
:raises ValueError: If :attr:`name` is ``None``, and not given in
args
:raises ValueError: If :attr:`url_part` is ``None``, and not given
in args, and :attr:`auto_url_part` is ``None``
or :attr:`name` is ``None``
:raises TypeError: If :func:`get_callback` not implemented
(see warning below)
.. warning ::
This method cannot be called on :class:`BaseRoute`, as
it is an abstract class missing an implementation of
:func:`get_callback` :
>>> try:
... BaseRoute()
... except Exception as catched:
... type(catched).__name__
... str(catched) == (
... "Can't instantiate abstract class BaseRoute "
... "with abstract methods get_callback"
... )
'TypeError'
True
"""
if name is not None:
self.name = name
elif self.name is None: # pragma: no cover
raise ValueError(
"No ``name`` argument provided to __init__"
", and no :attr:`name` defined as class attribute."
" (in {})".format(self)
)
if url_part is not None:
self.url_part = url_part
elif self.url_part is None:
if self.auto_url_part:
self.url_part = self.name
else:
raise ValueError( # pragma: no cover
"No ``url_part`` argument provided to __init__"
", no :attr:`url_part` defined as class attribute."
" (in {}), and :attr:`auto_url_part` is set to False."
"".format(self)
)
super().__init__(**kwargs)
self.redirect = self.get_url_name()
[docs] def patterns(self, parents=None,
add_redirect=None,
add_redirect_silent=None):
"""Yield patterns for URL regexs in :func:`get_url_regexs`, using
callback in :func:`get_callback` and URL name from
:func:`get_url_name`.
:argument parents: Not used in :class:`BaseRoute`'s implementation
of ``patterns``.
:type parents: list of :class:`django_crucrudile.routers.Router`
:argument add_redirect: Not used in :class:`BaseRoute`'s implementation
of ``patterns``.
:type add_redirect: bool
:argument add_redirect_silent: Not used in :class:`BaseRoute`'s
implementation
of ``patterns``.
:type add_redirect_silent: bool
:returns: Django URL patterns
:rtype: iterable of ``RegexURLPattern``
>>> class Route(BaseRoute):
... def get_callback(self):
... pass
>>>
>>> route = Route('name', 'url_part')
>>> list(route.patterns())
[<RegexURLPattern name ^url_part$>]
"""
callback = self.get_callback()
regexs_names = product(
self.get_url_regexs(),
self.get_url_names()
)
for regex, name in regexs_names:
yield url(
regex,
callback,
name=name
)
@abstractmethod
[docs] def get_callback(self): # pragma: no cover
"""Return callback to use in the URL pattern
.. warning::
Abstract method ! Should be implemented in subclasses,
otherwise class instantiation will fail. See warning in
:func:`__init__`.
:returns: Callable to use in the URL patter
:rtype: callable
"""
pass
[docs] def get_url_name(self):
"""Get the main URL name, defined at class level (:attr:`name`) or
passed to :func:`__init__`.
:returns: main URL name
:rtype: str
>>> class Route(BaseRoute):
... def get_callback(self):
... pass
>>>
>>> route = Route('name', 'url_part')
>>> route.get_url_name()
'name'
"""
return self.name
[docs] def get_url_names(self):
"""Get a list of URL names to generate patterns for. An least one URL
pattern will be returned for each URL name returned by this
function.
The default implementation returns a singleton (:func:`get_url_name`).
:returns: URL names (list of URL name)
:rtype: iterable of str
>>> class Route(BaseRoute):
... def get_callback(self):
... pass
>>>
>>> route = Route('name', 'url_part')
>>> print(list(route.get_url_names()))
['name']
"""
yield self.get_url_name()
[docs] def get_url_part(self):
"""Get the main URL part, defined at class level (:attr:`url_part`) or
passed to :func:`__init__`.
:returns: main URL part
:rtype: str
>>> class Route(BaseRoute):
... def get_callback(self):
... pass
>>>
>>> route = Route('name', 'url_part')
>>> route.get_url_part()
'url_part'
"""
return self.url_part
[docs] def get_url_parts(self):
"""Get a list of URL parts to generate patterns for. At least one URL
pattern will be returned for each URL part returned by this
function.
The default implementation returns a singleton (:func:`get_url_part`).
:returns: URL parts (list of URL part)
:rtype: iterable of str
>>> class Route(BaseRoute):
... def get_callback(self):
... pass
>>>
>>> route = Route('name', 'url_part')
>>> list(route.get_url_parts())
['url_part']
"""
yield self.get_url_part()
[docs] def get_url_specs(self):
"""Yield URL specifications. An URL specification is a 3-tuple,
containing 3 :class:`django_crucrudile.urlutils.URLBuilder` instances :
``prefix``, ``name`` and ``suffix``. These objects are used to
join together different part of the URLs. Using them in a
3-tuple allows building an URL part with a "central" name,
(whose parts are joined using ``-``), a prefix (joined using
``/``), and a suffix (joined using ``/``, ``/?``). This schema
allows different subclasses to register different parts in the
URLs (without interfering with each other, using
:func:`super()`), and provides automatic optional/required
separator handling.
The base implementation yields a specification for each URL
part returned by :func:`get_url_parts`.
.. note::
The ``prefix``, ``name`` and ``suffix`` names for the URL
specification contents are purely indicative, and never
used as identifiers. They are used in this package's code
for consistency.
:returns: URL specifications
:rtype: iterable of 3-tuple
>>> class Route(BaseRoute):
... def get_callback(self):
... pass
>>>
>>> route = Route('name', 'url_part')
>>> list(route.get_url_specs())
[([], ['url_part'], [])]
"""
prefix = URLBuilder(None, '/')
name = URLBuilder(None, '-')
suffix = URLBuilder(None, '/', '/?')
for part in self.get_url_parts():
if part is not None:
name.append(self.url_part)
yield prefix, name, suffix
[docs] def get_url_regexs(self):
"""Yield URL regexs to generate patterns for.
For each URL specification in :func:`get_url_specs` :
- Run each :class:`django_crucrudile.urlutils.URLBuilder`
instance in the URL specification 3-tuple (``prefix``,
``name`` and ``suffix``)
- Pass builder outputs in another URL builder
- Run this builder, and yield output, prefixed with '^' and
suffixed with '$'
URL specifications are structured as follow :
- iterable (list of
- iterable (3-tuple) of
- iterable (URLBuilder) of
- URL part
We use :class:`django_crucrudile.urlutils.URLBuilder` twice,
first to join the URL parts in each URL builder of the
specification (``prefix``, ``name`` and ``suffix``), and then
to join together the 3 resulting URL parts.
It's not possible to flatten this list directly, because
``prefix``, ``name`` and ``suffix`` may use different
separators.
:returns: URL regexs
:rtype: iterable of string
>>> class Route(BaseRoute):
... def get_callback(self):
... pass
>>>
>>> route = Route('name', 'url_part')
>>> list(route.get_url_regexs())
['^url_part$']
"""
for prefix, name, suffix in self.get_url_specs():
_prefix, _name, _suffix = (
part_list()
for part_list in (prefix, name, suffix)
)
builder = URLBuilder([_prefix, _name, _suffix])
required, built = builder()
yield '^{}$'.format(built)