diff --git a/csp/checks.py b/csp/checks.py index c7ca61a..0096b0e 100644 --- a/csp/checks.py +++ b/csp/checks.py @@ -2,7 +2,7 @@ import pprint from collections.abc import Sequence -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from typing import TYPE_CHECKING, Any from django.conf import settings from django.core.checks import Error @@ -47,9 +47,9 @@ ] -def migrate_settings() -> Tuple[Dict[str, Any], bool]: +def migrate_settings() -> tuple[dict[str, Any], bool]: # This function is used to migrate settings from the old format to the new format. - config: Dict[str, Any] = { + config: dict[str, Any] = { "DIRECTIVES": {}, } @@ -77,7 +77,7 @@ def migrate_settings() -> Tuple[Dict[str, Any], bool]: return config, REPORT_ONLY -def check_django_csp_lt_4_0(app_configs: Optional[Sequence[AppConfig]], **kwargs: Any) -> List[Error]: +def check_django_csp_lt_4_0(app_configs: Sequence[AppConfig] | None, **kwargs: Any) -> list[Error]: check_settings = OUTDATED_SETTINGS + ["CSP_REPORT_ONLY", "CSP_EXCLUDE_URL_PREFIXES", "CSP_REPORT_PERCENTAGE"] if any(hasattr(settings, setting) for setting in check_settings): # Try to build the new config. diff --git a/csp/context_processors.py b/csp/context_processors.py index 13f3808..b5eec54 100644 --- a/csp/context_processors.py +++ b/csp/context_processors.py @@ -1,12 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Literal +from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: from django.http import HttpRequest -def nonce(request: HttpRequest) -> Dict[Literal["CSP_NONCE"], str]: - nonce = request.csp_nonce if hasattr(request, "csp_nonce") else "" +def nonce(request: HttpRequest) -> dict[Literal["CSP_NONCE"], str]: + nonce = getattr(request, "csp_nonce", "") return {"CSP_NONCE": nonce} diff --git a/csp/decorators.py b/csp/decorators.py index bcac3b6..b330b6a 100644 --- a/csp/decorators.py +++ b/csp/decorators.py @@ -1,7 +1,7 @@ from __future__ import annotations from functools import wraps -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: from django.http import HttpRequest, HttpResponseBase @@ -11,7 +11,7 @@ _VIEW_DECORATOR_T = Callable[[_VIEW_T], _VIEW_T] -def csp_exempt(REPORT_ONLY: Optional[bool] = None) -> _VIEW_DECORATOR_T: +def csp_exempt(REPORT_ONLY: bool | None = None) -> _VIEW_DECORATOR_T: if callable(REPORT_ONLY): raise RuntimeError( "Incompatible `csp_exempt` decorator usage. This decorator now requires arguments, " @@ -42,7 +42,7 @@ def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: ) -def csp_update(config: Optional[Dict[str, Any]] = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T: +def csp_update(config: dict[str, Any] | None = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T: if config is None and kwargs: raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp_update")) @@ -61,7 +61,7 @@ def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: return decorator -def csp_replace(config: Optional[Dict[str, Any]] = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T: +def csp_replace(config: dict[str, Any] | None = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T: if config is None and kwargs: raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp_replace")) @@ -80,12 +80,12 @@ def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: return decorator -def csp(config: Optional[Dict[str, Any]] = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T: +def csp(config: dict[str, Any] | None = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T: if config is None and kwargs: raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp")) if config is None: - processed_config: Dict[str, List[Any]] = {} + processed_config: dict[str, list[Any]] = {} else: processed_config = {k: [v] if isinstance(v, str) else v for k, v in config.items()} diff --git a/csp/templatetags/csp.py b/csp/templatetags/csp.py index 8cdf83f..415bfcf 100644 --- a/csp/templatetags/csp.py +++ b/csp/templatetags/csp.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from django import template from django.template.base import token_kwargs @@ -38,7 +38,7 @@ def __init__(self, nodelist: NodeList, **kwargs: FilterExpression) -> None: for k, v in kwargs.items(): self.script_attrs[k] = self._get_token_value(v) - def _get_token_value(self, t: FilterExpression) -> Optional[str]: + def _get_token_value(self, t: FilterExpression) -> str | None: if hasattr(t, "token") and t.token: return _unquote(t.token) return None diff --git a/csp/tests/utils.py b/csp/tests/utils.py index bcd6120..dd139c3 100644 --- a/csp/tests/utils.py +++ b/csp/tests/utils.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple +from typing import TYPE_CHECKING, Any, Callable from django.http import HttpResponse from django.template import Context, Template, engines @@ -14,7 +14,7 @@ from django.http import HttpRequest -def response(*args: Any, headers: Optional[Dict[str, str]] = None, **kwargs: Any) -> Callable[[HttpRequest], HttpResponse]: +def response(*args: Any, headers: dict[str, str] | None = None, **kwargs: Any) -> Callable[[HttpRequest], HttpResponse]: def get_response(req: HttpRequest) -> HttpResponse: response = HttpResponse(*args, **kwargs) if headers: @@ -36,7 +36,7 @@ def assert_template_eq(self, tpl1: str, tpl2: str) -> None: bbb = tpl2.replace("\n", "").replace(" ", "") assert aaa == bbb, f"{aaa} != {bbb}" - def process_templates(self, tpl: str, expected: str) -> Tuple[str, str]: + def process_templates(self, tpl: str, expected: str) -> tuple[str, str]: request = rf.get("/") mw.process_request(request) nonce = getattr(request, "csp_nonce") diff --git a/csp/utils.py b/csp/utils.py index 723b967..378f83c 100644 --- a/csp/utils.py +++ b/csp/utils.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import copy import re from collections import OrderedDict from itertools import chain -from typing import Any, Callable, Dict, Optional, Union +from typing import Any, Callable, Dict from django.conf import settings from django.utils.encoding import force_str @@ -53,7 +55,7 @@ _DIRECTIVES = Dict[str, Any] -def default_config(csp: Optional[_DIRECTIVES]) -> Optional[_DIRECTIVES]: +def default_config(csp: _DIRECTIVES | None) -> _DIRECTIVES | None: if csp is None: return None # Make a copy of the passed in config to avoid mutating it, and also to drop any unknown keys. @@ -64,10 +66,10 @@ def default_config(csp: Optional[_DIRECTIVES]) -> Optional[_DIRECTIVES]: def build_policy( - config: Optional[_DIRECTIVES] = None, - update: Optional[_DIRECTIVES] = None, - replace: Optional[_DIRECTIVES] = None, - nonce: Optional[str] = None, + config: _DIRECTIVES | None = None, + update: _DIRECTIVES | None = None, + replace: _DIRECTIVES | None = None, + nonce: str | None = None, report_only: bool = False, ) -> str: """Builds the policy as a string from the settings.""" @@ -151,7 +153,7 @@ def _bool_attr_mapper(attr_name: str, val: bool) -> str: return "" -def _async_attr_mapper(attr_name: str, val: Union[str, bool]) -> str: +def _async_attr_mapper(attr_name: str, val: str | bool) -> str: """The `async` attribute works slightly different than the other bool attributes. It can be set explicitly to `false` with no surrounding quotes according to the spec.""" @@ -164,7 +166,7 @@ def _async_attr_mapper(attr_name: str, val: Union[str, bool]) -> str: # Allow per-attribute customization of returned string template -SCRIPT_ATTRS: Dict[str, Callable[[str, Any], str]] = OrderedDict() +SCRIPT_ATTRS: dict[str, Callable[[str, Any], str]] = OrderedDict() SCRIPT_ATTRS["nonce"] = _default_attr_mapper SCRIPT_ATTRS["id"] = _default_attr_mapper SCRIPT_ATTRS["src"] = _default_attr_mapper @@ -197,7 +199,7 @@ def _unwrap_script(text: str) -> str: return text -def build_script_tag(content: Optional[str] = None, **kwargs: Any) -> str: +def build_script_tag(content: str | None = None, **kwargs: Any) -> str: data = {} # Iterate all possible script attrs instead of kwargs to make # interpolation as easy as possible below diff --git a/pyproject.toml b/pyproject.toml index 1a2430d..b092e24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,9 +106,9 @@ lint.select = [ "I", # import sorting "Q", # flake8-quotes errors "T20", # flake8-print errors + "UP", # py-upgrade "W", # pycodestyle warnings # "B", # bugbear errors - incompatible with our use of `gettattr` and `setattr`. - # "UP", # py-upgrade - consider adding when we drop support for Python 3.8. ] # Allow unused variables when underscore-prefixed. lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"