Skip to content

Commit

Permalink
Expand the ruff config to include import sorting and others. (#234)
Browse files Browse the repository at this point in the history
* Update and move ruff config to pyproject.toml
* Bump pre-commit versions
* Run pyproject-fmt
* Run updated ruff config on all files
* Add `.python-version` to gitignore for pyenv + tox
* Add pyupgrade to ruff and fix errors
* Update CHANGELOG.md
  • Loading branch information
robhudson authored Jul 1, 2024
1 parent 2bb3e6d commit b348924
Show file tree
Hide file tree
Showing 20 changed files with 112 additions and 111 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*.sw[po]
.cache
.coverage
.python-version
.tox
dist
build
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.1
rev: v0.5.0
hooks:
# Run the linter
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
# Run the formatter
- id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
rev: 1.8.0
rev: 2.1.3
hooks:
- id: pyproject-fmt
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ CHANGES
=======

Unreleased
===========
==========
- Add type hints. ([#228](https://github.com/mozilla/django-csp/pull/228))
- Expand ruff configuration and move into pyproject.toml [[#234](https://github.com/mozilla/django-csp/pull/234)]

4.0b1
=====
Expand Down
10 changes: 6 additions & 4 deletions csp/checks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import pprint
from typing import Dict, Tuple, Any, Optional, Sequence, TYPE_CHECKING, List
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any

from django.conf import settings
from django.core.checks import Error
Expand Down Expand Up @@ -45,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": {},
}

Expand Down Expand Up @@ -75,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.
Expand Down
7 changes: 4 additions & 3 deletions csp/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from __future__ import annotations
from typing import Dict, Literal, TYPE_CHECKING

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}
3 changes: 2 additions & 1 deletion csp/contrib/rate_limiting.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING

import random
from typing import TYPE_CHECKING

from django.conf import settings

Expand Down
12 changes: 6 additions & 6 deletions csp/decorators.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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, "
Expand Down Expand Up @@ -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"))

Expand All @@ -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"))

Expand All @@ -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()}

Expand Down
3 changes: 2 additions & 1 deletion csp/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations
from typing import Callable, TYPE_CHECKING, Any

from typing import TYPE_CHECKING, Any, Callable

from jinja2 import nodes
from jinja2.ext import Extension
Expand Down
1 change: 1 addition & 0 deletions csp/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations

import base64
import http.client as http_client
import os
Expand Down
10 changes: 6 additions & 4 deletions csp/templatetags/csp.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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

from csp.utils import build_script_tag

if TYPE_CHECKING:
from django.template.base import NodeList, FilterExpression, Token, Parser
from django.template.base import FilterExpression, NodeList, Parser, Token
from django.template.context import Context

register = template.Library()
Expand All @@ -18,7 +20,7 @@ def _unquote(s: str) -> str:


@register.tag(name="script")
def script(parser: Parser, token: Token) -> "NonceScriptNode":
def script(parser: Parser, token: Token) -> NonceScriptNode:
# Parse out any keyword args
token_args = token.split_contents()
kwargs = token_kwargs(token_args[1:], parser)
Expand All @@ -36,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
Expand Down
3 changes: 2 additions & 1 deletion csp/tests/environment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from jinja2 import Environment
from typing import Any

from jinja2 import Environment


def environment(**options: Any) -> Environment:
env = Environment(**options)
Expand Down
1 change: 0 additions & 1 deletion csp/tests/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from csp.constants import NONCE, SELF


CONTENT_SECURITY_POLICY = {
"DIRECTIVES": {
"default-src": [SELF, NONCE],
Expand Down
4 changes: 3 additions & 1 deletion csp/tests/test_decorators.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import pytest
from django.http import HttpResponse
from django.test import RequestFactory
from django.test.utils import override_settings

import pytest

from csp.constants import HEADER, HEADER_REPORT_ONLY, NONCE
from csp.decorators import csp, csp_exempt, csp_replace, csp_update
from csp.middleware import CSPMiddleware
Expand Down
2 changes: 1 addition & 1 deletion csp/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.utils.functional import lazy

from csp.constants import NONCE, NONE, SELF
from csp.utils import build_policy, default_config, DEFAULT_DIRECTIVES
from csp.utils import DEFAULT_DIRECTIVES, build_policy, default_config


def policy_eq(a: str, b: str) -> None:
Expand Down
7 changes: 4 additions & 3 deletions csp/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Dict, Optional, TYPE_CHECKING, Callable, Any, Tuple
from typing import TYPE_CHECKING, Any, Callable

from django.http import HttpResponse
from django.template import Context, Template, engines
Expand All @@ -13,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:
Expand All @@ -35,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")
Expand Down
20 changes: 11 additions & 9 deletions csp/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import copy
import re
from collections import OrderedDict
from itertools import chain
from typing import Any, Dict, Optional, Union, Callable
from typing import Any, Callable, Dict

from django.conf import settings
from django.utils.encoding import force_str
Expand Down Expand Up @@ -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.
Expand All @@ -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."""
Expand Down Expand Up @@ -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."""
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ a more slightly strict policy and is used to test the policy without breaking th
signifies that you do not want any sources for this directive. The ``None`` value is a
Python keyword that represents the absence of a value and when used as the value of a directive,
it will remove the directive from the policy.

This is useful when using the ``@csp_replace`` decorator to effectively clear a directive from
the base configuration as defined in the settings. For example, if the Django settings the
``frame-ancestors`` directive is set to a list of sources and you want to remove the
Expand Down Expand Up @@ -124,9 +124,9 @@ policy.

The CSP keyword values of ``'self'``, ``'unsafe-inline'``, ``'strict-dynamic'``, etc. must be
quoted! e.g.: ``"default-src": ["'self'"]``. Without quotes they will not work as intended.

New in version 4.0 are CSP keyword constants. Use these to minimize quoting mistakes and typos.

The following CSP keywords are available:

* ``NONE`` = ``"'none'"``
Expand All @@ -140,9 +140,9 @@ policy.
* ``WASM_UNSAFE_EVAL`` = ``"'wasm-unsafe-eval'"``

Example usage:

.. code-block:: python
from csp.constants import SELF, STRICT_DYNAMIC
CONTENT_SECURITY_POLICY = {
Expand Down Expand Up @@ -318,4 +318,4 @@ the :ref:`decorator documentation <decorator-chapter>` for more details.
.. _block-all-mixed-content_mdn: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/block-all-mixed-content
.. _plugin_types_mdn: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/plugin-types
.. _prefetch_src_mdn: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/prefetch-src
.. _strict-csp: https://csp.withgoogle.com/docs/strict-csp.html
.. _strict-csp: https://csp.withgoogle.com/docs/strict-csp.html
6 changes: 3 additions & 3 deletions docs/migration-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,11 @@ The new settings would be:
.. note::

If you were using the ``CSP_INCLUDE_NONCE_IN`` setting, this has been removed in the new settings
format.
format.

**Previously:** You could use the ``CSP_INCLUDE_NONCE_IN`` setting to specify which directives in
your Content Security Policy (CSP) should include a nonce.

**Now:** You can include a nonce in any directive by adding the ``NONCE`` constant from the
``csp.constants`` module to the list of sources for that directive.

Expand Down
Loading

0 comments on commit b348924

Please sign in to comment.