Skip to content

Commit 3cce6f3

Browse files
offbyonenijel
andauthored
feat: Type Annotate the *heck* out of social-core (#986)
* Add py.typed and pyright to the build This enables typing in social_core and gives us the tools for testing * Many type annotation updates - ignore several errors that are endemic to mixin-type test strategies - in several cases assert that tests are set up correctly to give the type checker some help - for tidiness, mark some test-like class names as not-a-test * Most backends updated * Add type testing to the test action workflow * Ignore two very common type errors in the codebase * Restrict lxml to 5.2 xmlsec/python-xmlsec#320 * Undo a TODO that is not actually a bug * Correctly use __future__ annotations in models.py Co-authored-by: Michal Čihař <[email protected]>
1 parent 3aac098 commit 3cce6f3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+272
-74
lines changed

.github/workflows/test.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,43 @@ name: Tests
33
on: [push, pull_request]
44

55
jobs:
6+
types:
7+
runs-on: ubuntu-22.04
8+
strategy:
9+
fail-fast: false
10+
matrix:
11+
python-version:
12+
- '3.9'
13+
- '3.13'
14+
env:
15+
PYTHON_VERSION: ${{ matrix.python-version }}
16+
PYTHONUNBUFFERED: 1
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
persist-credentials: false
22+
23+
- name: Set up Python ${{ matrix.python-version }}
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: ${{ matrix.python-version }}
27+
cache: pip
28+
cache-dependency-path: requirements*.txt
29+
30+
- name: Install System dependencies
31+
run: |
32+
sudo apt-get update
33+
sudo apt-get install -qq -y --no-install-recommends libxmlsec1-dev swig
34+
35+
- name: Install Python dependencies
36+
run: |
37+
python -m pip install --upgrade pip
38+
pip install tox
39+
40+
- name: Type check with tox
41+
run: tox -e "py${PYTHON_VERSION/\./}-pyright"
42+
643
test:
744
runs-on: ubuntu-22.04
845
strategy:

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ Homepage = "https://github.com/python-social-auth/social-core"
119119
[project.optional-dependencies]
120120
saml = [
121121
"python3-saml>=1.5.0",
122+
# pinned to 5.2 until a new wheel of xmlsec is released
123+
"lxml~=5.2.1",
122124
]
123125
azuread = [
124126
"cryptography>=2.1.1",
@@ -140,6 +142,7 @@ dev = [
140142
"pytest-cov>=2.7.1",
141143
# pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484
142144
"urllib3~=2.2.0",
145+
"pyright>=1.1.391",
143146
]
144147

145148
[build-system]
@@ -155,4 +158,6 @@ dev = [
155158
"pytest-cov>=2.7.1",
156159
# pinned because of https://github.com/gabrielfalcao/HTTPretty/issues/484
157160
"urllib3~=2.2.0",
161+
"pyright>=1.1.391",
162+
"pytest-xdist>=3.6.1",
158163
]

pyrightconfig.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"reportOptionalMemberAccess": "none",
3+
"reportPossiblyUnboundVariable": "none"
4+
}

social_core/actions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ def do_complete(backend, login, user=None, redirect_name="next", *args, **kwargs
101101
else:
102102
url = setting_url(backend, "LOGIN_ERROR_URL", "LOGIN_URL")
103103

104+
assert url, "By this point URL has to have been set"
105+
104106
if redirect_value and redirect_value != url:
105107
redirect_value = quote(redirect_value)
106108
url += ("&" if "?" in url else "?") + f"{redirect_name}={redirect_value}"

social_core/backends/apple.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
login
2222
"""
2323

24+
from __future__ import annotations
25+
2426
import json
2527
import time
28+
from typing import TYPE_CHECKING
2629

2730
import jwt
2831
from jwt.algorithms import RSAAlgorithm
@@ -31,6 +34,9 @@
3134
from social_core.backends.oauth import BaseOAuth2
3235
from social_core.exceptions import AuthFailed
3336

37+
if TYPE_CHECKING:
38+
from jwt.types import JWKDict
39+
3440

3541
class AppleIdAuth(BaseOAuth2):
3642
name = "apple-id"
@@ -96,7 +102,7 @@ def get_key_and_secret(self):
96102
client_secret = self.generate_client_secret()
97103
return client_id, client_secret
98104

99-
def get_apple_jwk(self, kid=None):
105+
def get_apple_jwk(self, kid=None) -> str | JWKDict:
100106
"""
101107
Return requested Apple public key or all available.
102108
"""
@@ -107,7 +113,9 @@ def get_apple_jwk(self, kid=None):
107113

108114
if kid:
109115
return json.dumps(next(key for key in keys if key["kid"] == kid))
110-
return (json.dumps(key) for key in keys)
116+
# TODO: this should actually return a JWKDict; the caller expects it.
117+
# I suspect this code path is never hit in practice
118+
return (json.dumps(key) for key in keys) # type:ignore[reportReturnType]
111119

112120
def decode_id_token(self, id_token):
113121
"""
@@ -120,9 +128,10 @@ def decode_id_token(self, id_token):
120128
try:
121129
kid = jwt.get_unverified_header(id_token).get("kid")
122130
public_key = RSAAlgorithm.from_jwk(self.get_apple_jwk(kid))
131+
123132
decoded = jwt.decode(
124133
id_token,
125-
key=public_key,
134+
key=public_key, # type: ignore[reportArgumentType]
126135
audience=self.get_audience(),
127136
algorithms=["RS256"],
128137
)

social_core/backends/auth0.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def get_user_details(self, response):
6161
else:
6262
break
6363
else:
64+
assert signature_error is not None
6465
# raise last esception found during iteration
6566
raise signature_error
6667

social_core/backends/azuread_b2c.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ def jwt_key_to_pem(self, key_json_dict):
128128
Builds a PEM formatted key string from a JWT public key dict.
129129
"""
130130
pub_key = RSAAlgorithm.from_jwk(json.dumps(key_json_dict))
131-
return pub_key.public_bytes(
131+
132+
# TODO: clarify the types of this; JWKs can apparently include both public and private,
133+
# but this code assumes public.
134+
return pub_key.public_bytes( # type: ignore[reportAttributeAccessIssue]
132135
encoding=serialization.Encoding.PEM,
133136
format=serialization.PublicFormat.SubjectPublicKeyInfo,
134137
)

social_core/backends/azuread_tenant.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def user_data(self, access_token, *args, **kwargs):
9797

9898
return jwt_decode(
9999
id_token,
100-
key=certificate.public_key(),
100+
key=certificate.public_key(), # type: ignore[reportArgumentType]
101101
algorithms=["RS256"],
102102
audience=self.setting("KEY"),
103103
)

social_core/backends/base.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
from typing import Any
23

34
from requests import ConnectionError, request
45

@@ -118,7 +119,9 @@ def run_pipeline(self, pipeline, pipeline_index=0, *args, **kwargs):
118119
out.update(result)
119120
return out
120121

121-
def extra_data(self, user, uid, response, details=None, *args, **kwargs):
122+
def extra_data(
123+
self, user, uid, response, details=None, *args, **kwargs
124+
) -> dict[str, Any]:
122125
"""Return default extra data to store in extra_data field"""
123126
data = {
124127
# store the last time authentication toke place
@@ -137,9 +140,9 @@ def extra_data(self, user, uid, response, details=None, *args, **kwargs):
137140
size = len(entry)
138141
if size >= 1 and size <= 3:
139142
if size == 3:
140-
name, alias, discard = entry
143+
name, alias, discard = entry # type: ignore[reportAssignmentType]
141144
elif size == 2:
142-
(name, alias), discard = entry, False
145+
(name, alias), discard = entry, False # type: ignore[reportAssignmentType]
143146
elif size == 1:
144147
name = alias = entry[0]
145148
discard = False
@@ -213,7 +216,7 @@ def auth_extra_arguments(self):
213216
)
214217
return extra_arguments
215218

216-
def uses_redirect(self):
219+
def uses_redirect(self) -> bool:
217220
"""Return True if this provider uses redirect url method,
218221
otherwise return false."""
219222
return True

social_core/backends/bitbucket.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ class BitbucketOAuthBase:
1212

1313
def get_user_id(self, details, response):
1414
id_key = self.ID_KEY
15-
if self.setting("USERNAME_AS_ID", False):
15+
if self.setting("USERNAME_AS_ID", False): # type: ignore[reportAttributeAccessIssue]
1616
id_key = "username"
1717
return response.get(id_key)
1818

1919
def get_user_details(self, response):
2020
"""Return user details from Bitbucket account"""
21-
fullname, first_name, last_name = self.get_user_names(response["display_name"])
21+
fullname, first_name, last_name = self.get_user_names(response["display_name"]) # type: ignore[reportAttributeAccessIssue]
2222

2323
return {
2424
"username": response.get("username", ""),
@@ -38,7 +38,7 @@ def user_data(self, access_token, *args, **kwargs):
3838
if address["is_primary"]:
3939
break
4040

41-
if self.setting("VERIFIED_EMAILS_ONLY", False) and not address["is_confirmed"]:
41+
if self.setting("VERIFIED_EMAILS_ONLY", False) and not address["is_confirmed"]: # type: ignore[reportAttributeAccessIssue]
4242
raise AuthForbidden(self, "Bitbucket account has no verified email")
4343

4444
user = self._get_user(access_token)

0 commit comments

Comments
 (0)