Skip to content

Commit

Permalink
Add Python 3.12 support
Browse files Browse the repository at this point in the history
- Adds support for Python 3.12, by @bymoye
- Replaces `pkg_resources` with `importlib.resources` for all supported Python
  versions except for `3.8`.
- Runs tests against Pydantic `2.4.2` instead of Pydantic `2.0` to check
  support for Pydantic v2.
- Upgrades dependencies.
  • Loading branch information
bymoye authored Nov 3, 2023
1 parent d0a9f17 commit 310dc28
Show file tree
Hide file tree
Showing 18 changed files with 178 additions and 121 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.8, 3.9, "3.10", "3.11"]
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
runs-on: windows-latest
if: github.event_name == 'pull_request' || github.event_name == 'push'

Expand Down Expand Up @@ -71,7 +71,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: [3.8, 3.9, "3.10", "3.11"]
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
runs-on: ${{ matrix.os }}

steps:
Expand Down Expand Up @@ -101,7 +101,7 @@ jobs:
- name: Run tests with Pydantic v2
run: |
echo "[*] The previous tests used Pydantic v1, now running with v2"
pip install -U pydantic==2.0
pip install -U pydantic==2.4.2
pytest
- name: Run linters
Expand Down Expand Up @@ -161,7 +161,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.8, 3.9, "3.10", "3.11"]
python-version: [3.8, 3.9, "3.10", "3.11", "3.12"]
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0a12] - 2023-11-?? :fallen_leaf:

- Adds support for Python 3.12, by @bymoye
- Replaces `pkg_resources` with `importlib.resources` for all supported Python
versions except for `3.8`.
- Runs tests against Pydantic `2.4.2` instead of Pydantic `2.0` to check
support for Pydantic v2.
- Upgrades dependencies.

## [2.0a11] - 2023-09-19 :warning:

- Resolves bug in `2.0a10` caused by incompatibility issue with `Cython 3`.
Expand Down
9 changes: 5 additions & 4 deletions blacksheep/client/cookies.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
from datetime import datetime, timedelta
from datetime import timedelta
from ipaddress import ip_address
from typing import Dict, Iterable, Optional, TypeVar

from blacksheep import URL, Cookie
from blacksheep.utils.time import MIN_DATETIME, utcnow

client_logger = logging.getLogger("blacksheep.client")

Expand Down Expand Up @@ -41,7 +42,7 @@ class StoredCookie:
def __init__(self, cookie: Cookie):
# https://tools.ietf.org/html/rfc6265#section-5.3
self.cookie = cookie
self.creation_time = datetime.utcnow()
self.creation_time = utcnow()

expiry = None
if cookie.max_age > -1:
Expand All @@ -50,7 +51,7 @@ def __init__(self, cookie: Cookie):
# in the Cookie class
max_age = int(cookie.max_age)
if max_age <= 0:
expiry = datetime.min
expiry = MIN_DATETIME
else:
expiry = self.creation_time + timedelta(seconds=max_age)
elif cookie.expires:
Expand All @@ -65,7 +66,7 @@ def name(self):

def is_expired(self) -> bool:
expiration = self.expiry_time
if expiration and expiration < datetime.utcnow():
if expiration and expiration < utcnow():
return True

# NB: it's a 'session cookie'; in other words
Expand Down
3 changes: 2 additions & 1 deletion blacksheep/messages.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import charset_normalizer
from blacksheep.multipart import parse_multipart
from blacksheep.sessions import Session
from blacksheep.settings.json import json_settings
from blacksheep.utils.time import utcnow

from .contents cimport Content, multiparts_to_dictionary, parse_www_form_urlencoded
from .cookies cimport Cookie, parse_cookie, split_value, write_cookie_for_response
Expand Down Expand Up @@ -553,7 +554,7 @@ cdef class Response(Message):
Cookie(
name,
'',
datetime.utcnow() - timedelta(days=365)
utcnow() - timedelta(days=365)
)
)

Expand Down
11 changes: 10 additions & 1 deletion blacksheep/server/authentication/cookie.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from datetime import datetime

try:
from datetime import UTC
except ImportError:
from datetime import timezone

UTC = timezone.utc
from typing import Any, Optional, Sequence

from guardpost import AuthenticationHandler, Identity
Expand Down Expand Up @@ -80,7 +87,9 @@ def set_cookie(self, data: Any, response: Response, secure: bool = False) -> Non
path="/",
http_only=True,
secure=secure,
expires=datetime.fromtimestamp(data["exp"]) if "exp" in data else None,
expires=(
datetime.fromtimestamp(data["exp"], UTC) if "exp" in data else None
),
)
)

Expand Down
4 changes: 2 additions & 2 deletions blacksheep/server/openapi/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import json
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from http import HTTPStatus
from typing import (
Expand All @@ -33,6 +32,7 @@
from blacksheep.server.authorization import allow_anonymous
from blacksheep.server.files.static import get_response_for_static_content
from blacksheep.server.routing import Route, Router
from blacksheep.utils.time import utcnow

from .ui import SwaggerUIProvider, UIOptions, UIProvider

Expand Down Expand Up @@ -357,7 +357,7 @@ def get_spec_path(self) -> str:
)

def register_docs_handler(self, app: Application) -> None:
current_time = datetime.utcnow().timestamp()
current_time = utcnow().timestamp()

@self.ignore()
@allow_anonymous(self.anonymous_access)
Expand Down
6 changes: 3 additions & 3 deletions blacksheep/server/openapi/ui.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import Callable

from blacksheep.messages import Request, Response
from blacksheep.server.files.static import get_response_for_static_content
from blacksheep.server.resources import get_resource_file_content
from blacksheep.utils.time import utcnow


@dataclass
Expand Down Expand Up @@ -52,7 +52,7 @@ def build_ui(self, options: UIOptions) -> None:
self._ui_html = self.get_openapi_ui_html(options).encode("utf8")

def get_ui_handler(self) -> Callable[[Request], Response]:
current_time = datetime.utcnow().timestamp()
current_time = utcnow().timestamp()

def get_open_api_ui(request: Request) -> Response:
return get_response_for_static_content(
Expand Down Expand Up @@ -82,7 +82,7 @@ def build_ui(self, options: UIOptions) -> None:
self._ui_html = self.get_openapi_ui_html(options).encode("utf8")

def get_ui_handler(self) -> Callable[[Request], Response]:
current_time = datetime.utcnow().timestamp()
current_time = utcnow().timestamp()

def get_open_api_ui(request: Request) -> Response:
return get_response_for_static_content(
Expand Down
21 changes: 18 additions & 3 deletions blacksheep/server/resources.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
from os import path
"""
This module offers methods to return file paths for resources. Its original purpose is
to provide contents of static files stored for conveniency in the blacksheep.server.res
package.
"""

import pkg_resources
try:
from importlib.resources import files

def get_resource_file_path(anchor, file_name: str) -> str:
return str(files(anchor) / file_name)

except ImportError:
# Python 3.8
import pkg_resources

def get_resource_file_path(anchor, file_name: str) -> str:
return pkg_resources.resource_filename(anchor, file_name)


def get_resource_file_content(file_name: str) -> str:
with open(
pkg_resources.resource_filename(__name__, path.join(".", "res", file_name)),
get_resource_file_path("blacksheep.server.res", file_name),
mode="rt",
) as source:
return source.read()
12 changes: 12 additions & 0 deletions blacksheep/utils/time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import sys
from datetime import MINYEAR, datetime, timezone

UTC = timezone.utc

MIN_DATETIME = datetime(MINYEAR, 1, 1, tzinfo=None)


def utcnow() -> datetime:
if sys.version_info < (3, 12):
return datetime.utcnow()
return datetime.now(UTC).replace(tzinfo=None)
3 changes: 2 additions & 1 deletion itests/test_auth_oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from blacksheep.testing.messages import MockReceive, MockSend
from blacksheep.url import URL
from blacksheep.utils.aio import FailedRequestError
from blacksheep.utils.time import utcnow
from tests.test_auth import get_token
from tests.test_auth_cookie import get_auth_cookie
from tests.utils.application import FakeApplication
Expand Down Expand Up @@ -1324,7 +1325,7 @@ async def test_oidc_handler_logout_endpoint(
assert cookie_value is not None
cookie = parse_cookie(cookie_value)
assert cookie.expires is not None
assert cookie.expires < datetime.utcnow()
assert cookie.expires < utcnow()


def test_openid_configuration_class():
Expand Down
86 changes: 45 additions & 41 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,55 +1,59 @@
asgiref==3.5.2
attrs==22.1.0
certifi==2022.12.7
cffi==1.15.1
charset-normalizer==3.1.0
click==8.1.3
coverage==6.5.0
cryptography==41.0.0
Cython==3.0.2; platform_system != "Windows"
annotated-types==0.6.0
asgiref==3.7.2
attrs==23.1.0
blinker==1.6.3
certifi==2023.7.22
cffi==1.16.0
charset-normalizer==3.3.1
click==8.1.7
coverage==7.3.2
cryptography==41.0.4
Cython==3.0.4; platform_system != "Windows"
essentials==1.1.5
essentials-openapi==1.0.6
Flask==2.2.3
gevent==22.10.1
greenlet==1.1.3.post0
guardpost~=1.0.0
h11==0.11.0
essentials-openapi==1.0.8
Flask==3.0.0
gevent==23.9.1
greenlet==3.0.0
guardpost==1.0.2
h11==0.14.0
h2==4.1.0
hpack==4.0.0
httptools==0.5.0
hypercorn==0.14.3
httptools==0.6.1
hypercorn==0.14.4
hyperframe==6.0.1
idna==3.4
iniconfig==1.1.1
iniconfig==2.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
mccabe==0.6.1
packaging==21.3
pluggy==1.0.0
MarkupSafe==2.1.3
mccabe==0.7.0
packaging==23.2
pluggy==1.3.0
priority==2.0.0
py==1.11.0
pycparser==2.21
pydantic==1.10.2
PyJWT==2.6.0
pyparsing==3.0.9
pytest==7.3.1
pytest-asyncio==0.20.1
pytest-cov==4.0.0
pydantic==2.4.2
pydantic_core==2.10.1
PyJWT==2.8.0
pyparsing==3.1.1
pytest==7.4.2
pytest-asyncio==0.21.1
pytest-cov==4.1.0
python-dateutil==2.8.2
PyYAML==6.0
rodi~=2.0.2
regex==2023.3.23
requests==2.28.2
PyYAML==6.0.1
regex==2023.10.3
requests==2.31.0
rodi==2.0.3
setuptools==68.2.2
six==1.16.0
toml==0.10.1
toml==0.10.2
tomli==2.0.1
typing_extensions==4.4.0
urllib3==1.26.12
uvicorn==0.21.1
wcwidth==0.2.5
websockets==11.0.2
Werkzeug==2.2.2
typing_extensions==4.8.0
urllib3==2.0.7
uvicorn==0.23.2
wcwidth==0.2.8
websockets==12.0
Werkzeug==3.0.0
wsproto==1.2.0
zope.event==4.5.0
zope.interface==5.5.0
zope.event==5.0
zope.interface==6.1
Loading

0 comments on commit 310dc28

Please sign in to comment.