Skip to content

Commit 3933107

Browse files
committed
Add type hints
1 parent d9845e4 commit 3933107

9 files changed

Lines changed: 208 additions & 92 deletions

File tree

HISTORY.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ unreleased
1010
- Use GitHub Actions for CI.
1111
[jugmac00]
1212

13-
- Switch to PEP-420 namespace package
13+
- Switch to PEP-420 namespace package.
14+
[Daverball]
15+
16+
- Add type hints.
1417
[Daverball]
1518

1619
0.2.0 (2018-02-02)

more/content_security/core.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,43 @@
1+
from __future__ import annotations
2+
13
import base64
24
import os
5+
from typing import TYPE_CHECKING, Literal
36

47
from morepath import App
58
from morepath.request import Request
69
from more.content_security.policy import ContentSecurityPolicy
710

11+
if TYPE_CHECKING:
12+
from collections.abc import Callable
13+
from typing_extensions import TypeVar
14+
15+
from webob import Response as BaseResponse
16+
17+
from morepath.types import Tween
18+
19+
_AppT = TypeVar(
20+
"_AppT",
21+
bound="ContentSecurityApp",
22+
default="ContentSecurityApp",
23+
covariant=True,
24+
)
25+
else:
26+
from typing import TypeVar
27+
28+
_AppT = TypeVar("_AppT", bound="ContentSecurityApp", covariant=True)
29+
830
# see https://csp.withgoogle.com/docs/faq.html#generating-nonces
931
NONCE_LENGTH = 16
1032

1133

12-
def random_nonce():
34+
def random_nonce() -> str:
1335
return base64.b64encode(os.urandom(NONCE_LENGTH)).decode("utf-8")
1436

1537

16-
class ContentSecurityRequest(Request):
38+
class ContentSecurityRequest(Request[_AppT]):
1739
@property
18-
def content_security_policy(self):
40+
def content_security_policy(self) -> ContentSecurityPolicy:
1941
"""Provides access to a request-local version of the content
2042
security policy.
2143
@@ -32,10 +54,10 @@ def content_security_policy(self):
3254
return self._content_security_policy
3355

3456
@content_security_policy.setter
35-
def content_security_policy(self, policy):
57+
def content_security_policy(self, policy: ContentSecurityPolicy) -> None:
3658
self._content_security_policy = policy
3759

38-
def content_security_policy_nonce(self, target):
60+
def content_security_policy_nonce(self, target: Literal["script", "style"]) -> str:
3961
"""Generates a nonce that's random once per request, adds it to
4062
either 'style-src' or 'script-src' and returns its value.
4163
@@ -57,7 +79,7 @@ def content_security_policy_nonce(self, target):
5779
return nonce
5880

5981
@property
60-
def content_security_policy_nonce_value(self):
82+
def content_security_policy_nonce_value(self) -> str:
6183
"""Returns the request-bound content security nonce. It is secure
6284
to keep this once per request. It is only dangerous to use nonces
6385
over more than one request.
@@ -78,23 +100,29 @@ class ContentSecurityApp(App):
78100

79101

80102
@ContentSecurityApp.setting("content_security_policy", "default")
81-
def default_policy():
103+
def default_policy() -> ContentSecurityPolicy:
82104
return ContentSecurityPolicy()
83105

84106

85107
@ContentSecurityApp.setting("content_security_policy", "apply_policy")
86-
def default_policy_apply_factory():
87-
def apply_policy(policy, request, response):
108+
def default_policy_apply_factory() -> (
109+
Callable[[ContentSecurityPolicy, Request, BaseResponse], None]
110+
):
111+
def apply_policy(
112+
policy: ContentSecurityPolicy, request: Request, response: BaseResponse
113+
) -> None:
88114
policy.apply(response)
89115

90116
return apply_policy
91117

92118

93119
@ContentSecurityApp.tween_factory()
94-
def content_security_policy_tween_factory(app, handler):
120+
def content_security_policy_tween_factory(
121+
app: ContentSecurityApp, handler: Tween
122+
) -> Tween:
95123
policy_settings = app.settings.content_security_policy
96124

97-
def content_security_policy_tween(request):
125+
def content_security_policy_tween(request: ContentSecurityRequest) -> BaseResponse:
98126
response = handler(request)
99127

100128
if hasattr(request, "_content_security_policy"):

more/content_security/policy.py

Lines changed: 84 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1+
from __future__ import annotations
2+
13
import inspect
24

35
from copy import deepcopy
6+
from typing import TYPE_CHECKING, Any, Generic, TypeGuard, TypeVar
7+
8+
if TYPE_CHECKING:
9+
from collections.abc import Callable, Generator
10+
from typing_extensions import Self
11+
12+
from webob.response import Response as BaseResponse
13+
14+
_T = TypeVar("_T")
415

516
SELF = "'self'"
617
UNSAFE_INLINE = "'unsafe-inline'"
@@ -9,7 +20,7 @@
920
STRICT_DYNAMIC = "'strict-dynamic'"
1021

1122

12-
class Directive:
23+
class Directive(Generic[_T]):
1324
"""Descriptor for the management and rendering of CSP directives.
1425
1526
Uses types to do some basic sanity checking. This does not ensure
@@ -20,13 +31,19 @@ class Directive:
2031
2132
"""
2233

23-
def __init__(self, name, type, default, render):
34+
def __init__(
35+
self,
36+
name: str,
37+
type: type[_T],
38+
default: Callable[[], _T],
39+
render: Callable[[_T], str | None],
40+
) -> None:
2441
self.name = name
2542
self.type = type
2643
self.default = default
2744
self.renderer = render
2845

29-
def render(self, instance):
46+
def render(self, instance: ContentSecurityPolicy) -> str | None:
3047
if self.name not in instance.__dict__:
3148
return None
3249

@@ -35,7 +52,9 @@ def render(self, instance):
3552

3653
return self.renderer(instance.__dict__[self.name])
3754

38-
def __get__(self, instance, cls):
55+
def __get__(
56+
self, instance: ContentSecurityPolicy, cls: type[ContentSecurityPolicy]
57+
) -> _T:
3958
if instance is None:
4059
return self
4160

@@ -44,40 +63,37 @@ def __get__(self, instance, cls):
4463

4564
return instance.__dict__[self.name]
4665

47-
def __set__(self, instance, value):
66+
def __set__(self, instance: ContentSecurityPolicy, value: _T) -> None:
4867
if not isinstance(value, self.type):
4968
raise TypeError(f"Expected type {self.type}")
5069

5170
instance.__dict__[self.name] = value
5271

5372

54-
class SetDirective(Directive):
55-
def __init__(self, name):
56-
parent = super()
57-
parent.__init__(name, type=set, default=set, render=render_set)
73+
class SetDirective(Directive[set[str]]):
74+
def __init__(self, name: str) -> None:
75+
super().__init__(name, type=set, default=set, render=render_set)
5876

5977

60-
class SingleValueDirective(Directive):
61-
def __init__(self, name):
62-
parent = super()
63-
parent.__init__(name, type=str, default=str, render=str)
78+
class SingleValueDirective(Directive[str]):
79+
def __init__(self, name: str) -> None:
80+
super().__init__(name, type=str, default=str, render=str)
6481

6582

66-
class BooleanDirective(Directive):
67-
def __init__(self, name):
68-
parent = super()
69-
parent.__init__(name, type=bool, default=bool, render=render_bool)
83+
class BooleanDirective(Directive[bool]):
84+
def __init__(self, name: str) -> None:
85+
super().__init__(name, type=bool, default=bool, render=render_bool)
7086

7187

72-
def is_directive(obj):
88+
def is_directive(obj: object) -> TypeGuard[Directive[Any]]:
7389
return isinstance(obj, Directive)
7490

7591

76-
def render_set(value):
92+
def render_set(value: set[str]) -> str:
7793
return " ".join(sorted(value))
7894

7995

80-
def render_bool(value):
96+
def render_bool(value: bool) -> str | None:
8197
return "" if value else None
8298

8399

@@ -103,39 +119,52 @@ class ContentSecurityPolicy:
103119
"""
104120

105121
# Fetch directives
106-
child_src = SetDirective("child-src")
107-
connect_src = SetDirective("connect-src")
108-
default_src = SetDirective("default-src")
109-
font_src = SetDirective("font-src")
110-
frame_src = SetDirective("frame-src")
111-
img_src = SetDirective("img-src")
112-
manifest_src = SetDirective("manifest-src")
113-
media_src = SetDirective("media-src")
114-
object_src = SetDirective("object-src")
115-
script_src = SetDirective("script-src")
116-
style_src = SetDirective("style-src")
117-
worker_src = SetDirective("worker-src")
122+
child_src: SetDirective = SetDirective("child-src")
123+
connect_src: SetDirective = SetDirective("connect-src")
124+
default_src: SetDirective = SetDirective("default-src")
125+
font_src: SetDirective = SetDirective("font-src")
126+
frame_src: SetDirective = SetDirective("frame-src")
127+
img_src: SetDirective = SetDirective("img-src")
128+
manifest_src: SetDirective = SetDirective("manifest-src")
129+
media_src: SetDirective = SetDirective("media-src")
130+
object_src: SetDirective = SetDirective("object-src")
131+
script_src: SetDirective = SetDirective("script-src")
132+
style_src: SetDirective = SetDirective("style-src")
133+
worker_src: SetDirective = SetDirective("worker-src")
118134

119135
# Document directives
120-
base_uri = SetDirective("base-uri")
121-
plugin_types = SetDirective("plugin-types")
122-
sandbox = SingleValueDirective("sandbox")
123-
disown_opener = BooleanDirective("disown-opener")
136+
base_uri: SetDirective = SetDirective("base-uri")
137+
plugin_types: SetDirective = SetDirective("plugin-types")
138+
sandbox: SingleValueDirective = SingleValueDirective("sandbox")
139+
disown_opener: BooleanDirective = BooleanDirective("disown-opener")
124140

125141
# Navigation directives
126-
form_action = SetDirective("form-action")
127-
frame_ancestors = SetDirective("frame-ancestors")
142+
form_action: SetDirective = SetDirective("form-action")
143+
frame_ancestors: SetDirective = SetDirective("frame-ancestors")
128144

129145
# Reporting directives
130-
report_uri = SingleValueDirective("report-uri")
131-
report_to = SingleValueDirective("report-to")
146+
report_uri: SingleValueDirective = SingleValueDirective("report-uri")
147+
report_to: SingleValueDirective = SingleValueDirective("report-to")
132148

133149
# Other directives
134-
block_all_mixed_content = BooleanDirective("block-all-mixed-content")
135-
require_sri_for = SingleValueDirective("require-sri-for")
136-
upgrade_insecure_requeists = BooleanDirective("upgrade-insecure-requests")
137-
138-
def __init__(self, report_only=False, **directives):
150+
block_all_mixed_content: BooleanDirective = BooleanDirective(
151+
"block-all-mixed-content"
152+
)
153+
require_sri_for: SingleValueDirective = SingleValueDirective("require-sri-for")
154+
upgrade_insecure_requeists: BooleanDirective = BooleanDirective(
155+
"upgrade-insecure-requests"
156+
)
157+
158+
def __init__(
159+
self,
160+
report_only: bool = False,
161+
# NOTE: This is both a little too lax and a little too strict, but
162+
# it doesn't seem worth defining a TypedDict, to get better
163+
# type checking on this, this will work for most cases and
164+
# is not the recommended style of defining the directives
165+
# anyways.
166+
**directives: set[str] | str | bool,
167+
) -> None:
139168
self.report_only = report_only
140169

141170
for directive in directives:
@@ -144,32 +173,35 @@ def __init__(self, report_only=False, **directives):
144173
assert hasattr(self, name)
145174
setattr(self, name, directives[directive])
146175

147-
def copy(self):
176+
def copy(self) -> Self:
148177
policy = self.__class__()
149178
policy.__dict__ = deepcopy(self.__dict__)
150179

151180
return policy
152181

153182
@property
154-
def directives(self):
183+
def directives(self) -> Generator[Directive[Any]]:
155184
for name, value in inspect.getmembers(self.__class__, is_directive):
156185
yield value
157186

158187
@property
159-
def text(self):
160-
values = ((d.name, d.render(self)) for d in self.directives)
161-
values = ((name, text) for name, text in values if text is not None)
188+
def text(self) -> str:
189+
values = (
190+
(d.name, text)
191+
for d in self.directives
192+
if (text := d.render(self)) is not None
193+
)
162194

163195
return ";".join(" ".join(v).strip() for v in values)
164196

165197
@property
166-
def header_name(self):
198+
def header_name(self) -> str:
167199
if self.report_only:
168200
return "Content-Security-Policy-Report-Only"
169201
else:
170202
return "Content-Security-Policy"
171203

172-
def apply(self, response):
204+
def apply(self, response: BaseResponse) -> None:
173205
text = self.text
174206

175207
if text:

more/content_security/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)