Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try running tests on Python 3.14. #216

Closed
wants to merge 5 commits into from
Closed
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
12 changes: 8 additions & 4 deletions .github/workflows/tests-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14.0-alpha.4']
env:
- TOXENV: py
include:
Expand All @@ -33,15 +33,19 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install tox
- name: Install -dev for 3.14
if: contains(matrix.python-version, '3.14')
run: |
sudo apt-get update
sudo apt-get install libxml2-dev libxslt-dev
- name: tox
env: ${{ matrix.env }}
run: |
tox
- name: coverage
uses: codecov/codecov-action@v4
if: ${{ success() }}
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}

check:
runs-on: ubuntu-latest
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/tests-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ jobs:
run: |
tox
- name: coverage
uses: codecov/codecov-action@v4
if: ${{ success() }}
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@ repos:
- flake8-string-format
repo: https://github.com/pycqa/flake8
rev: 7.1.1
- hooks:
- id: pyupgrade
args: [--py39-plus]
exclude: ^docs/tutorial-project/
repo: https://github.com/asottile/pyupgrade
rev: v3.19.1
8 changes: 5 additions & 3 deletions tests/po_lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
This package is just for overrides testing purposes.
"""

from typing import Any, Dict, List, Type, Union
from __future__ import annotations

from typing import Any

from url_matcher import Patterns

Expand All @@ -13,10 +15,10 @@


class POBase(ItemPage):
expected_instead_of: Union[Type[ItemPage], List[Type[ItemPage]]]
expected_instead_of: type[ItemPage] | list[type[ItemPage]]
expected_patterns: Patterns
expected_to_return: Any = None
expected_meta: Dict[str, Any]
expected_meta: dict[str, Any]


class POTopLevelOverriden1(ItemPage): ...
Expand Down
8 changes: 5 additions & 3 deletions tests/po_lib_sub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
external depedencies.
"""

from typing import Any, Dict, Type
from __future__ import annotations

from typing import Any

from url_matcher import Patterns

from web_poet import ItemPage, handle_urls


class POBase(ItemPage):
expected_instead_of: Type[ItemPage]
expected_instead_of: type[ItemPage]
expected_patterns: Patterns
expected_meta: Dict[str, Any]
expected_meta: dict[str, Any]


class POLibSubOverriden(ItemPage): ...
Expand Down
16 changes: 9 additions & 7 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import asyncio
import random
from typing import Callable, List
from typing import Callable

import attrs
import pytest
Expand Down Expand Up @@ -629,11 +631,11 @@ def proc1(s):

class BasePage(ItemPage):
class Processors:
f1: List[Callable] = [str.strip]
f1: list[Callable] = [str.strip]
f2 = [str.strip]
f3 = [str.strip]
f4: List[Callable] = [str.strip]
f5: List[Callable] = [str.strip]
f4: list[Callable] = [str.strip]
f5: list[Callable] = [str.strip]

@field
def f1(self):
Expand Down Expand Up @@ -695,7 +697,7 @@ def desc(self):

class Page(BasePage):
class Processors(BasePage.Processors):
name: List[Callable] = []
name: list[Callable] = []

@field
def name(self):
Expand All @@ -704,8 +706,8 @@ def name(self):

class Page2(Page):
class Processors(Page.Processors):
name: List[Callable] = []
desc: List[Callable] = []
name: list[Callable] = []
desc: list[Callable] = []

@field
def desc(self):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_input_validation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Test page object input validation scenarios."""

from typing import Optional
from __future__ import annotations

import attrs
import pytest
Expand Down Expand Up @@ -278,7 +278,7 @@ async def a(self):
async def test_invalid_input_cross_api_caching():
@attrs.define
class _Item(Item):
b: Optional[str] = None
b: str | None = None

class Page(BaseCachingPage, Returns[_Item]):
@field
Expand Down
14 changes: 6 additions & 8 deletions tests/test_page_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_http_response_body_json() -> None:
http_body = HttpResponseBody(b'{"foo": 123}')
assert http_body.json() == {"foo": 123}

http_body = HttpResponseBody('{"ключ": "значение"}'.encode("utf8"))
http_body = HttpResponseBody('{"ключ": "значение"}'.encode())
assert http_body.json() == {"ключ": "значение"}


Expand Down Expand Up @@ -295,7 +295,7 @@ def test_http_response_json() -> None:
response = HttpResponse(url, body=b'{"key": "value"}')
assert response.json() == {"key": "value"}

response = HttpResponse(url, body='{"ключ": "значение"}'.encode("utf8"))
response = HttpResponse(url, body='{"ключ": "значение"}'.encode())
assert response.json() == {"ключ": "значение"}


Expand Down Expand Up @@ -343,19 +343,17 @@ def test_http_response_utf16() -> None:


def test_explicit_encoding() -> None:
response = HttpResponse(
"http://www.example.com", "£".encode("utf-8"), encoding="utf-8"
)
response = HttpResponse("http://www.example.com", "£".encode(), encoding="utf-8")
assert response.encoding == "utf-8"
assert response.text == "£"


def test_explicit_encoding_invalid() -> None:
response = HttpResponse(
"http://www.example.com", body="£".encode("utf-8"), encoding="latin1"
"http://www.example.com", body="£".encode(), encoding="latin1"
)
assert response.encoding == "latin1"
assert response.text == "£".encode("utf-8").decode("latin1")
assert response.text == "£".encode().decode("latin1")


def test_utf8_body_detection() -> None:
Expand Down Expand Up @@ -386,7 +384,7 @@ def test_gb2312() -> None:
def test_bom_encoding() -> None:
response = HttpResponse(
"http://www.example.com",
body=codecs.BOM_UTF8 + "🎉".encode("utf-8"),
body=codecs.BOM_UTF8 + "🎉".encode(),
headers={"Content-type": "text/html; charset=cp1251"},
)
assert response.encoding == "utf-8"
Expand Down
8 changes: 5 additions & 3 deletions tests/test_pages.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Generic, List, Optional, TypeVar
from __future__ import annotations

from typing import Generic, TypeVar

import attrs
import pytest
Expand Down Expand Up @@ -143,7 +145,7 @@ async def test_item_page_required_field_missing() -> None:
@attrs.define
class MyItem:
name: str
price: Optional[float]
price: float | None

class MyPage(ItemPage[MyItem]):
@field
Expand Down Expand Up @@ -267,7 +269,7 @@ class BookItem:

@attrs.define
class ListItem:
books: List[BookItem]
books: list[BookItem]

@attrs.define
class MyPage(ItemPage[ListItem]):
Expand Down
6 changes: 4 additions & 2 deletions tests/test_requests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Callable, Union
from __future__ import annotations

from typing import Callable
from unittest import mock

import pytest
Expand Down Expand Up @@ -98,7 +100,7 @@ async def test_http_client_allow_status(

method = getattr(client, method_name)

url_or_request: Union[str, HttpRequest] = "url"
url_or_request: str | HttpRequest = "url"
if method_name == "execute":
# NOTE: We're ignoring the type below due to the following mypy bugs:
# - https://github.com/python/mypy/issues/10187
Expand Down
5 changes: 3 additions & 2 deletions tests/test_serialization.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Annotated, Type
# from __future__ import annotations breaks some tests here
from typing import Annotated

import attrs
import pytest
Expand Down Expand Up @@ -153,7 +154,7 @@ def __init__(self, value: int):
def _serialize(o: C) -> SerializedLeafData:
return {"bin": o.value.to_bytes((o.value.bit_length() + 7) // 8, "little")}

def _deserialize(t: Type[C], data: SerializedLeafData) -> C:
def _deserialize(t: type[C], data: SerializedLeafData) -> C:
return t(int.from_bytes(data["bin"], "little"))

register_serialization(_serialize, _deserialize)
Expand Down
14 changes: 8 additions & 6 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

import datetime
import json
from collections import deque
from pathlib import Path
from typing import Annotated, Any, Dict, Optional
from typing import Annotated, Any
from zoneinfo import ZoneInfo

import attrs
Expand Down Expand Up @@ -31,7 +33,7 @@ def test_save_fixture(book_list_html_response, tmp_path) -> None:
meta = {"foo": "bar", "frozen_time": "2022-01-01"}

def _assert_fixture_files(
directory: Path, expected_meta: Optional[dict] = None
directory: Path, expected_meta: dict | None = None
) -> None:
input_dir = directory / INPUT_DIR_NAME
assert (input_dir / "HttpResponse-body.html").exists()
Expand Down Expand Up @@ -218,13 +220,13 @@ def test_pytest_plugin_compare_item_fail(pytester, book_list_html_response) -> N

@attrs.define(kw_only=True)
class MetadataLocalTime(Metadata):
dateDownloadedLocal: Optional[str] = None
dateDownloadedLocal: str | None = None


@attrs.define(kw_only=True)
class ProductLocalTime(Product):
# in newer zyte-common-items this should inherit from ProductMetadata
metadata: Optional[MetadataLocalTime] # type: ignore[assignment]
metadata: MetadataLocalTime | None # type: ignore[assignment]


def _get_product_item(date: datetime.datetime) -> ProductLocalTime:
Expand All @@ -250,10 +252,10 @@ async def to_item(self) -> Item:

def _assert_frozen_item(
frozen_time: datetime.datetime,
pytester: "pytest.Pytester",
pytester: pytest.Pytester,
response: HttpResponse,
*,
outcomes: Optional[Dict[str, int]] = None,
outcomes: dict[str, int] | None = None,
) -> None:
# this makes an item with datetime fields corresponding to frozen_time
item = ItemAdapter(_get_product_item(frozen_time)).asdict()
Expand Down
2 changes: 2 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import asyncio
import inspect
import random
Expand Down
8 changes: 5 additions & 3 deletions tests_extra/po_lib_sub_not_imported/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
captures the rules annotated in this module if it was not imported.
"""

from typing import Any, Dict, Type
from __future__ import annotations

from typing import Any

from url_matcher import Patterns

from web_poet import ItemPage, handle_urls


class POBase:
expected_instead_of: Type[ItemPage]
expected_instead_of: type[ItemPage]
expected_patterns: Patterns
expected_meta: Dict[str, Any]
expected_meta: dict[str, Any]


class POLibSubOverridenNotImported: ...
Expand Down
10 changes: 6 additions & 4 deletions web_poet/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
In general, users shouldn't import and use the contents of this module.
"""

from typing import AnyStr, Dict, List, Tuple, Type, TypeVar, Union
from __future__ import annotations

from typing import AnyStr, TypeVar, Union

from multidict import CIMultiDict

_AnyStrDict = Dict[AnyStr, Union[AnyStr, List[AnyStr], Tuple[AnyStr, ...]]]
_AnyStrDict = dict[AnyStr, Union[AnyStr, list[AnyStr], tuple[AnyStr, ...]]]
T_headers = TypeVar("T_headers", bound="_HttpHeaders")


Expand All @@ -19,7 +21,7 @@ class _HttpHeaders(CIMultiDict):
"""

@classmethod
def from_name_value_pairs(cls: Type[T_headers], arg: List[Dict]) -> T_headers:
def from_name_value_pairs(cls: type[T_headers], arg: list[dict]) -> T_headers:
"""An alternative constructor for instantiation using a ``List[Dict]``
where the 'key' is the header name while the 'value' is the header value.

Expand All @@ -35,7 +37,7 @@ def from_name_value_pairs(cls: Type[T_headers], arg: List[Dict]) -> T_headers:

@classmethod
def from_bytes_dict(
cls: Type[T_headers], arg: _AnyStrDict, encoding: str = "utf-8"
cls: type[T_headers], arg: _AnyStrDict, encoding: str = "utf-8"
) -> T_headers:
"""An alternative constructor for instantiation where the header-value
pairs could be in raw bytes form.
Expand Down
Loading