PK!u֥url_checks/__init__.py"""Django Check ensures URL paths end with a slash""" from .checks import check_url # noqa: F401 __version__ = "0.1.0" default_app_config = "url_checks.apps.UrlChecksConfig" PK!˸Aurl_checks/apps.py"""AppConfig for django-url-checks package""" from django.apps import AppConfig class UrlChecksConfig(AppConfig): """AppConfig for django-url-checks package""" name = "url_checks" def ready(self): """Import checks once apps are loaded Note that the register decorator does the actual work of informing Django about the check. """ from .checks import check_url # noqa: F401 PK!S6 url_checks/checks.py"""Django Check ensures URL paths end with a slash https://docs.djangoproject.com/en/2.1/topics/checks/ """ from importlib import import_module from django.conf import settings from django.core.checks import register from django.urls import URLPattern, URLResolver from .messages import MESSAGES __all__ = ["check_url"] def _get_url_tree(): """Get Root URL Configuration for project""" try: url_mod_dot_path = settings.ROOT_URLCONF except AttributeError: return (None, [MESSAGES.URLCONF_NOT_DEFINED()]) try: url_mod = import_module(url_mod_dot_path) except AttributeError: return (None, [MESSAGES.URLCONF_WRONG_FORMAT()]) except ModuleNotFoundError: return (None, [MESSAGES.URLCONF_NOT_FOUND()]) try: return (url_mod.urlpatterns, None) except AttributeError: return (None, [MESSAGES.URLCONF_NO_PATTERNS()]) def _check_path(path, namespace): """Verify that a single path ends with a slash""" pattern = str(path.pattern) if len(pattern) > 0 and not pattern.endswith(("/", "/$")): name = f"{namespace}:{path.name}" if namespace else path.name return MESSAGES.NO_SLASH_PATTERN( hint=f'Add slash to end of path "{pattern}", named "{name}"' ) def _check_resolver(path, namespace): """Verify that an include path ends with a slash""" pattern = str(path.pattern) if len(pattern) > 0 and not pattern.endswith(("/", "/$")): return MESSAGES.NO_SLASH_RESOLVER( hint=f'Add slash to end of include path "{pattern}"' ) def _check_path_tree(url_tree, namespace=None): """Provide error for each URI pattern missing a / at end""" errors = [] for path in url_tree: if isinstance(path, URLPattern): msg = _check_path(path, namespace) if msg: errors.append(msg) elif isinstance(path, URLResolver): new_namespace = ( f"{namespace}:{path.namespace}" if namespace and path.namespace else (path.namespace or namespace) ) msg = _check_resolver(path, namespace) if msg: errors.append(msg) errors.extend(_check_path_tree(path.url_patterns, new_namespace)) else: errors.append(MESSAGES.URLCONF_UNKNOWN_TYPE()) return errors @register() def check_url(*args, **kwargs): """Ensure all URI paths end with a '/' character""" url_tree, messages = _get_url_tree() return messages or _check_path_tree(url_tree) PK!T9url_checks/messages.py"""All of the messages used by the check""" from functools import partial from django.core.checks import ( Critical, Debug, Error, Info, Warning as CheckWarning, ) from .messages_text import CRITICAL_PROTO_MSG, ERROR_PROTO_MSG __all__ = ["MESSAGES"] def get_message_letter(Message): """Return expected character code for Django Message""" if Message is Critical: return "C" elif Message is Error: return "E" elif Message is CheckWarning: return "W" elif Message is Info: return "I" elif Message is Debug: return "D" else: raise KeyError() def omit_keys(*args): """Remove keys from a dictionary""" *keys, dict_obj = args return { field: value for field, value in dict_obj.items() if field not in keys } def build_messages(app_name, Message, message_list): """Build messages dictionary of specific type""" letter = get_message_letter(Message) return { msg.var: partial( Message, id=f"{app_name}.{letter}{i:0{3}}", **omit_keys("var", msg._asdict()), ) for i, msg in enumerate(message_list, start=1) } class Messages: """Singleton container for all messages""" def __init__(self, app_name="url_checks"): self._messages = dict( # this construction ensures no duplicate keys **build_messages(app_name, Critical, CRITICAL_PROTO_MSG), **build_messages(app_name, Error, ERROR_PROTO_MSG), ) def __getattr__(self, name): if name in self._messages: return self._messages[name] raise AttributeError(name) MESSAGES = Messages() PK!Պ!url_checks/messages_text.py"""Text constants to be used in messages""" from typing import NamedTuple, Optional __all__ = ["CRITICAL_PROTO_MSG", "ERROR_PROTO_MSG"] class ProtoMsg(NamedTuple): """Prototype Message, for generating IDs automatically""" var: str # variable attribute name msg: str # string message hint: Optional[str] # hint BASIC_URLCONF_MSG = ( "Define ROOT_URLCONF in your settings as a string pointing to your" ' URL paths (e.g.: ROOT_URLCONF="config.urls")' ) CRITICAL_PROTO_MSG = [ ProtoMsg( "URLCONF_NOT_DEFINED", "Root UrlConf setting not defined", BASIC_URLCONF_MSG, ), ProtoMsg( "URLCONF_WRONG_FORMAT", "Root UrlConf setting unexpected format", BASIC_URLCONF_MSG, ), ProtoMsg( "URLCONF_NOT_FOUND", "Root UrlConf module not found", "Point ROOT_URLCONF in your settings to an existing module in your" ' code (e.g.: ROOT_URLCONF="config.urls" points Django to ' "/config/urls.py)", ), ProtoMsg( "URLCONF_NO_PATTERNS", "Root UrlConf missing urlpatterns", "Your root URL configuration must contain a variable named" " urlpatterns with a list of URL paths. (e.g.: urlpatterns=[" 'path("", home_view, name="home")]', ), ProtoMsg( "URLCONF_UNKNOWN_TYPE", "Your URL configuration tree contains an unknwon type." " Only expected use of path() and include()", None, ), ] ERROR_PROTO_MSG = [ ProtoMsg("NO_SLASH_PATTERN", "URI pattern does not end with slash", None), ProtoMsg( "NO_SLASH_RESOLVER", "URI include pattern does not end with slash", None, ), ] PK!܋,,)django_url_checks-0.1.0.dist-info/LICENSEBSD 2-Clause License Copyright (c) 2018, JamBon Software LLC All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. PK!HnHTU'django_url_checks-0.1.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H)ypK**django_url_checks-0.1.0.dist-info/METADATAS]o0|XHTpx"g2NΊ<)!P,[r7o^VJ tY ; ϹyKTݹÜHjU#^PK!HuL(django_url_checks-0.1.0.dist-info/RECORDл@gk+( 6 BM~iw!SN][ J{͓ް5aƼ@~:RuY2-.jmD^M'Rr@Rūr t_ϝ++)0TL, e6ԎM!V tOVF0L28&S.kfM]%]+]wyXjϑOL(8COIeN*b7f\fIvq ?&9=q{2.I <(Kv, ԑm>G)MΪSZ?SWUlI;zb[TusnSb܉'(Gȝf`(^~gZj= H-PK!u֥url_checks/__init__.pyPK!˸Aurl_checks/apps.pyPK!S6 url_checks/checks.pyPK!T9 url_checks/messages.pyPK!Պ!url_checks/messages_text.pyPK!܋,,)django_url_checks-0.1.0.dist-info/LICENSEPK!HnHTU'\ django_url_checks-0.1.0.dist-info/WHEELPK!H)ypK** django_url_checks-0.1.0.dist-info/METADATAPK!HuL(#django_url_checks-0.1.0.dist-info/RECORDPK %