Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
25 changes: 6 additions & 19 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,8 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: [3.8, 3.9, 3.10.2, 3.11, 3.12]
sqlalchemy-version: [1.3, 1.4]
django-version: [2.2, 4.2]
exclude:
- python-version: '3.8'
sqlalchemy-version: '1.4'
- python-version: '3.9'
sqlalchemy-version: '1.4'
- python-version: '3.8'
django-version: '4.2'
- python-version: '3.9'
django-version: '4.2'
name: Test Py ${{ matrix.python-version }}, SQLA ${{ matrix.sqlalchemy-version }}, Django ${{ matrix.django-version }}
python-version: [3.11, 3.12, 3.13]
name: Test Py ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -32,19 +21,17 @@ jobs:
version: 1.3.2
- name: Install dependencies
run: |
sed -i.bak 's/^SQLAlchemy = .*/SQLAlchemy = "^${{ matrix.sqlalchemy-version }}"/' pyproject.toml
sed -i.bak 's/^Django = .*/Django = "^${{ matrix.django-version }}"/' pyproject.toml
poetry install
- name: Test with pytest
run: |
poetry run pytest -rfs --cov --cov-config=.coveragerc --cov-report="" --disable-warnings
cp .coverage ".coverage.${{ matrix.python-version }}-${{ matrix.sqlalchemy-version }}-${{ matrix.django-version }}"
cp .coverage ".coverage.${{ matrix.python-version }}"
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-reports-${{ matrix.python-version }}-${{ matrix.sqlalchemy-version }}-${{ matrix.django-version }}
name: coverage-reports-${{ matrix.python-version }}
include-hidden-files: true
path: ".coverage.${{ matrix.python-version }}-${{ matrix.sqlalchemy-version }}-${{ matrix.django-version }}"
path: ".coverage.${{ matrix.python-version }}"

coverage-check:
name: Coverage check
Expand All @@ -58,7 +45,7 @@ jobs:
python-version: 3.12
- name: Install dependencies
run: |
pip3 install coverage==7.2.3
pip3 install coverage==7.6.12
- name: Download coverage reports
uses: actions/download-artifact@v4
with:
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ 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).

## [30.0.0] - 2025-02-27
- Dropped support of Python earlier than 3.11
- Dropped support of Django earlier than 4.2
- Dropped support of SQLAlchemy earlier than 1.4
- Added support of Python 3.13
- Reviewed new typing-related things since Python 3.8:
- Added support of `Union[A, B]` → `A | B`
- Added support of `List[T]` → `list[T]`, `Dict[K, V]` → `dict[K, V]`, etc.
- Reviewed `Never` and `NoReturn` types - they are only for forever-looping functions, no sense to add their support
- Reviewed `Self` return type support - not suitable for endpoints, they do not return classes
- Added test for `TypeAlias`
- Reviewed `TypeGuard` - not suitable for endpoints
- Reviewed `LiteralString` - not suitable for endpoints, they usually return strings
- Reviewed `Concatenate` - not suitable for endpoints
- Added test for `TypedDict`

## [29.0.2] - 2025-02-07
- Add support of UnionType in OpenAPI schema

Expand Down
18 changes: 7 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "winter"
version = "29.0.2"
version = "30.0.0"
homepage = "https://github.com/WinterFramework/winter"
description = "Web Framework with focus on python typing, dataclasses and modular design"
authors = ["Alexander Egorov <[email protected]>"]
Expand All @@ -11,14 +11,10 @@ classifiers = [
'Operating System :: OS Independent',
'Environment :: Web Environment',
'Programming Language :: Python',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Framework :: Django',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3',
'Framework :: Django :: 4',
'Topic :: Software Development :: Libraries :: Application Frameworks',
]
Expand All @@ -31,13 +27,13 @@ packages = [
]

[tool.poetry.dependencies]
python = "^3.8"
Django = ">=2"
python = "^3.11"
Django = ">=4.2,<5"
docstring-parser = ">=0.1"
furl = ">=2.0.0, <3"
python-dateutil = "^2.8.2"
injector = ">=0.15.0, <1"
SQLAlchemy = ">=1.3, <2"
SQLAlchemy = ">=1.4, <2"
typing-extensions = "^4.8"
StrEnum = "^0.4.8"
openapi-pydantic = ">=0.5.0, <0.6"
Expand All @@ -51,11 +47,11 @@ flake8 = ">=3.7.7, <4"
flake8-commas = ">=2.0.0, <4"
flake8-formatter-abspath = ">=1.0.1, <2"
pre-commit-hooks = ">=2.2.3, <3"
freezegun = ">=0.3.15, <1"
freezegun = ">=1.5.1, <2"
mock = ">=2.0.0, <3"
pytest = ">=6.2.5, <7"
pytest-pythonpath = ">=0.7.1"
pytest-cov = ">=2.5.1, <3"
pytest-cov = "^6.0.0"
pytest-django = ">=3.2.0, <4"
semver = ">=2.8.1, <3"
add-trailing-comma = ">=1.3.0, <2"
Expand Down
15 changes: 11 additions & 4 deletions tests/api/api_with_query_parameters.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
import datetime
from typing import Any

from typing import Dict
from typing import List
from typing import Optional
from typing import TypeAlias
from uuid import UUID

import winter

ArrayAlias: TypeAlias = list[int]


@winter.route('with-query-parameter')
class APIWithQueryParameters:

@winter.map_query_parameter('string', to='mapped_string')
@winter.route_get('/{?date,boolean,optional_boolean,date_time,array,expanded_array*,string,uid}')
@winter.route_get('/{?date,boolean,optional_boolean,optional_boolean_new_typing_style,date_time,array,array_new_typing_style,array_alias,expanded_array*,string,uid}')
def root(
self,
date: datetime.date,
date_time: datetime.datetime,
boolean: bool,
array: List[int],
array_new_typing_style: list[int],
array_alias: ArrayAlias,
expanded_array: List[str],
mapped_string: str,
uid: UUID,
optional_boolean: Optional[bool] = None,
) -> Dict[str, Any]:
optional_boolean_new_typing_style: bool | None = None,
) -> dict[str, Any]:
return {
'date': date,
'date_time': date_time,
'boolean': boolean,
'optional_boolean': optional_boolean,
'optional_boolean_new_typing_style': optional_boolean_new_typing_style,
'array': array,
'array_new_typing_style': array_new_typing_style,
'array_alias': array_alias,
'expanded_array': expanded_array,
'string': mapped_string,
'uid': str(uid),
Expand Down
17 changes: 17 additions & 0 deletions tests/api/api_with_request_data.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import enum
from typing import List
from typing import NotRequired
from typing import Optional

import dataclasses
from typing import Required
from typing import TypeAlias
from typing import TypedDict

import winter

Expand All @@ -12,15 +16,28 @@ class Status(enum.Enum):
SUPER_ACTIVE = 'super_active'


ItemsTypeAlias: TypeAlias = list[int]


class TypedDictExample(TypedDict):
field: str
required_field: Required[int]
optional_field: NotRequired[int]


@dataclasses.dataclass
class Data:
id: int
name: str
is_god: bool
optional_status: Optional[Status]
optional_status_new_typing_style: Status | None
status: Status
items: List[int]
items_alias: ItemsTypeAlias
optional_items: Optional[List[int]]
optional_items_new_typing_style: list[int] | None
typed_dict: TypedDictExample
with_default: int = 5


Expand Down
16 changes: 16 additions & 0 deletions tests/core/json/test_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class User:
emails: List[str] = dataclasses.field(default_factory=list)
created_at: Optional[datetime.datetime] = None
name: str = 'test name'
comments: list[str] | None = None


@dataclasses.dataclass(frozen=True)
Expand All @@ -79,6 +80,7 @@ class Profile:
'contact': {
'phones': ['123', '456'],
},
'comments': ['comment1', 'comment2'],
},
User(
Id(1),
Expand All @@ -89,6 +91,7 @@ class Profile:
['[email protected]'],
datetime.datetime(year=2001, month=2, day=3, hour=4, minute=5, second=6, tzinfo=tzutc()),
'name',
['comment1', 'comment2'],
),
),
(
Expand Down Expand Up @@ -127,7 +130,9 @@ def test_decode(data, expected_instance):
@pytest.mark.parametrize(
('data', 'type_', 'expected_instance'), (
(['super'], Set[Status], {Status.SUPER}),
(['super'], set[Status], {Status.SUPER}),
([1], Set, {1}),
([1], set, {1}),
),
)
def test_decode_set(data, type_, expected_instance):
Expand All @@ -150,6 +155,8 @@ def test_decode_sequence(data, type_, expected_instance):
('data', 'type_', 'expected_instance'), (
(None, Optional[Status], None),
('super', Optional[Status], Status.SUPER),
(None, Status | None, None),
('super', Status | None, Status.SUPER),
),
)
def test_decode_optional(data, type_, expected_instance):
Expand Down Expand Up @@ -180,6 +187,7 @@ def test_decode_set_with_errors(data, type_, expected_errors):
(['super'], List[Status], [Status.SUPER]),
(['1'], List[IntStatus], [IntStatus.SUPER]),
([1], List[IntStatus], [IntStatus.SUPER]),
([1], list[IntStatus], [IntStatus.SUPER]),
({1}, List, [1]),
({1}, list, [1]),
),
Expand All @@ -198,6 +206,7 @@ def test_decode_list(data, type_, expected_instance):
),
(1, List[Status], 'Cannot decode "1" to list'),
(None, List[Status], 'Cannot decode "None" to list'),
(None, list[Status], 'Cannot decode "None" to list'),
(None, List[IntStatus], 'Cannot decode "None" to list'),
(['a'], List[IntStatus], 'Value not in allowed values("1", "2"): "a"'),
),
Expand Down Expand Up @@ -465,6 +474,11 @@ class DataclassWithUndefinedType:
a: Union[int, Undefined]


@dataclasses.dataclass
class DataclassWithUndefinedTypeNewTypingStyle:
a: int | Undefined


@dataclasses.dataclass
class DataclassWithUndefinedByDefault:
a: Union[int, Undefined] = Undefined()
Expand All @@ -474,6 +488,8 @@ class DataclassWithUndefinedByDefault:
('data', 'type_', 'expected_result'), (
({}, DataclassWithUndefinedType, DataclassWithUndefinedType(a=Undefined())),
({'a': 123}, DataclassWithUndefinedType, DataclassWithUndefinedType(a=123)),
({}, DataclassWithUndefinedTypeNewTypingStyle, DataclassWithUndefinedTypeNewTypingStyle(a=Undefined())),
({'a': 123}, DataclassWithUndefinedTypeNewTypingStyle, DataclassWithUndefinedTypeNewTypingStyle(a=123)),
({}, DataclassWithUndefinedByDefault, DataclassWithUndefinedByDefault(a=Undefined())),
({'a': 123}, DataclassWithUndefinedByDefault, DataclassWithUndefinedByDefault(a=123)),
),
Expand Down
12 changes: 10 additions & 2 deletions tests/routing/test_query_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,27 +183,33 @@ def test_query_parameter(api_client, date, date_time, boolean, optional_boolean,
'date_time': '2019-05-01T22:28:31',
'boolean': boolean == 'true',
'optional_boolean': optional_boolean == 'true' if optional_boolean is not None else None,
'optional_boolean_new_typing_style': optional_boolean == 'true' if optional_boolean is not None else None,
'array': array,
'array_new_typing_style': array,
'array_alias': array,
'expanded_array': list(map(str, array)),
'string': string,
'uid': str(uid),
}
base_uri = URITemplate(
'/with-query-parameter/'
'{?date,date_time,boolean,optional_boolean,array,expanded_array*,string,uid}',
'{?date,date_time,boolean,optional_boolean,optional_boolean_new_typing_style,array,array_new_typing_style,array_alias,expanded_array*,string,uid}',
)
query_params = {
'date': date,
'date_time': date_time,
'boolean': boolean,
'array': ','.join(map(str, array)),
'array_new_typing_style': ','.join(map(str, array)),
'array_alias': ','.join(map(str, array)),
'expanded_array': array,
'string': string,
'uid': uid,
}

if optional_boolean is not None:
query_params['optional_boolean'] = optional_boolean
query_params['optional_boolean_new_typing_style'] = optional_boolean

base_uri = base_uri.expand(**query_params)

Expand All @@ -215,13 +221,15 @@ def test_query_parameter(api_client, date, date_time, boolean, optional_boolean,
def test_invalid_uuid_query_parameter_triggers_400(api_client):
base_uri = URITemplate(
'/with-query-parameter/'
'{?date,date_time,boolean,optional_boolean,array,expanded_array*,string,uid}',
'{?date,date_time,boolean,optional_boolean,array,array_new_typing_style,array_alias,expanded_array*,string,uid}',
)
query_params = {
'date': datetime.datetime.now().date(),
'date_time': datetime.datetime.now(),
'boolean': 'true',
'array': '5',
'array_new_typing_style': '5',
'array_alias': '5',
'expanded_array': ['5'],
'string': '',
'uid': str(uuid4()) + 'a',
Expand Down
Loading
Loading