Skip to content
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
17 changes: 4 additions & 13 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,9 @@ 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'
python-version: [3.11, 3.12, 3.13]
sqlalchemy-version: [ 1.4 ]
django-version: [ 4.2 ]
name: Test Py ${{ matrix.python-version }}, SQLA ${{ matrix.sqlalchemy-version }}, Django ${{ matrix.django-version }}
steps:
- uses: actions/checkout@v3
Expand Down Expand Up @@ -58,7 +49,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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ 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`
- Added tests of new typing features for event system

## [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
48 changes: 38 additions & 10 deletions tests/messaging/test_simple_event_publisher.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from typing import List
from typing import Union

from dataclasses import dataclass
from injector import ClassProvider
from injector import singleton

Expand Down Expand Up @@ -69,6 +68,12 @@ def handler8(self, events: List[Union[Event1, Event3]]):
self.result['handlers'].append(8)
self.result['x'] += event.x

@event_handler
def handler9(self, events: list[Event1 | Event3]):
for event in events:
self.result['handlers'].append(9)
self.result['x'] += event.x


def test_simple_event_publisher():
injector = get_injector()
Expand All @@ -87,37 +92,60 @@ def test_simple_event_publisher():
# Assert
# emit event
result = event_handlers.result
assert result == {'handlers': [1, 2, 7, 8], 'x': 40}
assert result == {'handlers': [1, 2, 7, 8, 9], 'x': 50}

# emit same event again
simple_event_publisher.emit(Event1(20))
assert result == {'handlers': [1, 2, 7, 8, 1, 2, 7, 8], 'x': 120}
assert result == {'handlers': [1, 2, 7, 8, 9, 1, 2, 7, 8, 9], 'x': 150}

# emit event with no handlers
simple_event_publisher.emit(Event2(100))
assert result == {'handlers': [1, 2, 7, 8, 1, 2, 7, 8], 'x': 120}
assert result == {'handlers': [1, 2, 7, 8, 9, 1, 2, 7, 8, 9], 'x': 150}

# emit event with other annotation
simple_event_publisher.emit(Event3(200))
assert result == {'handlers': [1, 2, 7, 8, 1, 2, 7, 8, 3, 7, 8], 'x': 720}
assert result == {'handlers': [1, 2, 7, 8, 9, 1, 2, 7, 8, 9, 3, 7, 8, 9], 'x': 950}

# emit event with handler for single event and list of events
simple_event_publisher.emit(Event4(1000))
assert result == {'handlers': [1, 2, 7, 8, 1, 2, 7, 8, 3, 7, 8, 5, 6], 'x': 2720}
assert result == {'handlers': [1, 2, 7, 8, 9, 1, 2, 7, 8, 9, 3, 7, 8, 9, 5, 6], 'x': 2950}

# Test emit_many
event_handlers.result = {'handlers': [], 'x': 0}

# emit event
simple_event_publisher.emit_many([Event1(10)])
result = event_handlers.result
assert result == {'handlers': [1, 2, 7, 8], 'x': 40}
assert result == {'handlers': [1, 2, 7, 8, 9], 'x': 50}

# emit same event twice
simple_event_publisher.emit_many([Event3(200), Event3(200)])
assert result == {'handlers': [1, 2, 7, 8, 3, 3, 7, 7, 8, 8], 'x': 1240}
assert result == {'handlers': [1, 2, 7, 8, 9, 3, 3, 7, 7, 8, 8, 9, 9], 'x': 1650}

# emit different events
simple_event_publisher.emit_many([Event1(10), Event2(100), Event3(200), Event4(1000)])
assert result == {'handlers': [1, 2, 7, 8, 3, 3, 7, 7, 8, 8, 1, 2, 7, 7, 8, 8, 3, 5, 6], 'x': 3880}

assert result == {
'handlers': [1, 2,
7,
8,
9,
3,
3,
7,
7,
8,
8,
9,
9,
1,
2,
7,
7,
8,
8,
9,
9,
3,
5,
6], 'x': 4500
}
Loading
Loading