Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand the ruff config to include import sorting and others. #234

Merged
merged 8 commits into from
Jul 1, 2024
Merged
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
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]:
Copy link
Member

Choose a reason for hiding this comment

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

It's interesting that this is allowed with Python 3.8 with from __future__ import annotations

# 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:
Copy link
Member

Choose a reason for hiding this comment

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

Nice catch!

# 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