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
7 changes: 7 additions & 0 deletions .changeset/drop_support_for_python_39.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
default: major
---

# Drop support for Python 3.9

Both `openapi-python-client` itself and any generated clients no longer support Python 3.9.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
default: major
---

# Generated models now use `from __future__ import annotations`

This simplifies using forward references with the newer union syntax.
5 changes: 5 additions & 0 deletions .changeset/minimum_typer_version_is_now_016.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: note
---

# Minimum Typer version is now 0.16
7 changes: 7 additions & 0 deletions .changeset/upgrade_generated_clients_to_310_union_syntax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
default: minor
---

# Upgrade generated clients to 3.10 union syntax

All generated types now use the `A | B` syntax instead of `Union[A, B]` or `Optional[A]`.
14 changes: 7 additions & 7 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
test:
strategy:
matrix:
python: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
python: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]
os: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
Expand Down Expand Up @@ -78,7 +78,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6.0.0
with:
python-version: "3.9"
python-version: "3.10"

- name: Get Python Version
id: get_python_version
Expand Down Expand Up @@ -128,15 +128,15 @@ jobs:

# Find all of the downloaded coverage reports and combine them
.venv/bin/python -m coverage combine

# Create html report
.venv/bin/python -m coverage html --skip-covered --skip-empty

# Report in Markdown and write to summary.
.venv/bin/python -m coverage report --format=markdown >> $GITHUB_STEP_SUMMARY

# Report again and fail if under 100%.
.venv/bin/python -m coverage report --fail-under=100
.venv/bin/python -m coverage report --fail-under=100

- name: Upload HTML report if check failed.
uses: actions/upload-artifact@v4.6.2
Expand All @@ -163,7 +163,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6.0.0
with:
python-version: "3.9"
python-version: "3.10"
- name: Get Python Version
id: get_python_version
run: echo "python_version=$(python --version)" >> $GITHUB_OUTPUT
Expand Down
10 changes: 5 additions & 5 deletions end_to_end_tests/__snapshots__/test_end_to_end.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# name: test_documents_with_errors[bad-status-code]
'''
Generating /test-documents-with-errors
Warning(s) encountered while generating. Client was generated, but some pieces may be missing

Warning(s) encountered while generating. Client was generated, but some pieces may be missing
WARNING parsing GET / within default.

Invalid response status code pattern: abcdef, response will be omitted from generated client
Expand All @@ -16,8 +16,8 @@
# name: test_documents_with_errors[circular-body-ref]
'''
Generating /test-documents-with-errors
Warning(s) encountered while generating. Client was generated, but some pieces may be missing

Warning(s) encountered while generating. Client was generated, but some pieces may be missing
WARNING parsing POST / within default. Endpoint will not be generated.

Circular $ref in request body
Expand All @@ -30,8 +30,8 @@
# name: test_documents_with_errors[invalid-uuid-defaults]
'''
Generating /test-documents-with-errors
Warning(s) encountered while generating. Client was generated, but some pieces may be missing

Warning(s) encountered while generating. Client was generated, but some pieces may be missing
WARNING parsing PUT / within default. Endpoint will not be generated.

cannot parse parameter of endpoint put_: Invalid UUID value: 3
Expand All @@ -49,8 +49,8 @@
# name: test_documents_with_errors[missing-body-ref]
'''
Generating /test-documents-with-errors
Warning(s) encountered while generating. Client was generated, but some pieces may be missing

Warning(s) encountered while generating. Client was generated, but some pieces may be missing
WARNING parsing POST / within default. Endpoint will not be generated.

Could not resolve $ref #/components/requestBodies/body in request body
Expand All @@ -63,8 +63,8 @@
# name: test_documents_with_errors[optional-path-param]
'''
Generating /test-documents-with-errors
Warning(s) encountered while generating. Client was generated, but some pieces may be missing

Warning(s) encountered while generating. Client was generated, but some pieces may be missing
WARNING parsing GET /{optional} within default. Endpoint will not be generated.

Path parameter must be required
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ssl
from typing import Any, Optional, Union
from typing import Any

import httpx
from attrs import define, evolve, field
Expand Down Expand Up @@ -34,12 +34,12 @@ class Client:
_base_url: str = field(alias="base_url")
_cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
_headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
_timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
_verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl")
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
_httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
_client: Optional[httpx.Client] = field(default=None, init=False)
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
_client: httpx.Client | None = field(default=None, init=False)
_async_client: httpx.AsyncClient | None = field(default=None, init=False)

def with_headers(self, headers: dict[str, str]) -> "Client":
"""Get a new client matching this one with additional headers"""
Expand Down Expand Up @@ -157,12 +157,12 @@ class AuthenticatedClient:
_base_url: str = field(alias="base_url")
_cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
_headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
_timeout: Optional[httpx.Timeout] = field(default=None, kw_only=True, alias="timeout")
_verify_ssl: Union[str, bool, ssl.SSLContext] = field(default=True, kw_only=True, alias="verify_ssl")
_timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
_verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl")
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
_httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
_client: Optional[httpx.Client] = field(default=None, init=False)
_async_client: Optional[httpx.AsyncClient] = field(default=None, init=False)
_client: httpx.Client | None = field(default=None, init=False)
_async_client: httpx.AsyncClient | None = field(default=None, init=False)

token: str
"""The token to use for authentication"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any, TypeVar, Union
from typing import Any, TypeVar

from attrs import define as _attrs_define
from attrs import field as _attrs_field
Expand All @@ -13,10 +15,10 @@
class ModelWithDescription:
"""This is a nice model."""

prop_with_no_desc: Union[Unset, str] = UNSET
prop_with_desc: Union[Unset, str] = UNSET
prop_with_no_desc: str | Unset = UNSET
prop_with_desc: str | Unset = UNSET
""" This is a nice property. """
prop_with_long_desc: Union[Unset, str] = UNSET
prop_with_long_desc: str | Unset = UNSET
""" It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of
foolishness,
it was the epoch of belief, it was the epoch of incredulity, it was the season of light, it was the season of
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any, TypeVar, Union
from typing import Any, TypeVar

from attrs import define as _attrs_define
from attrs import field as _attrs_field
Expand All @@ -11,8 +13,8 @@

@_attrs_define
class ModelWithNoDescription:
prop_with_no_desc: Union[Unset, str] = UNSET
prop_with_desc: Union[Unset, str] = UNSET
prop_with_no_desc: str | Unset = UNSET
prop_with_desc: str | Unset = UNSET
""" This is a nice property. """
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from collections.abc import Mapping, MutableMapping
from http import HTTPStatus
from typing import IO, BinaryIO, Generic, Literal, Optional, TypeVar, Union
from typing import IO, BinaryIO, Generic, Literal, TypeVar

from attrs import define

Expand All @@ -15,13 +15,13 @@ def __bool__(self) -> Literal[False]:
UNSET: Unset = Unset()

# The types that `httpx.Client(files=)` can accept, copied from that library.
FileContent = Union[IO[bytes], bytes, str]
FileTypes = Union[
FileContent = IO[bytes] | bytes | str
FileTypes = (
# (filename, file (or bytes), content_type)
tuple[Optional[str], FileContent, Optional[str]],
tuple[str | None, FileContent, str | None]
# (filename, file (or bytes), content_type, headers)
tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
]
| tuple[str | None, FileContent, str | None, Mapping[str, str]]
)
RequestFiles = list[tuple[str, FileTypes]]


Expand All @@ -30,8 +30,8 @@ class File:
"""Contains information for file uploads"""

payload: BinaryIO
file_name: Optional[str] = None
mime_type: Optional[str] = None
file_name: str | None = None
mime_type: str | None = None

def to_tuple(self) -> FileTypes:
"""Return a tuple representation that httpx will accept for multipart/form-data"""
Expand All @@ -48,7 +48,7 @@ class Response(Generic[T]):
status_code: HTTPStatus
content: bytes
headers: MutableMapping[str, str]
parsed: Optional[T]
parsed: T | None


__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"]
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ packages = [
include = ["CHANGELOG.md", "my_test_api_client/py.typed"]

[tool.poetry.dependencies]
python = "^3.9"
python = "^3.10"
httpx = ">=0.23.0,<0.29.0"
attrs = ">=22.2.0"
python-dateutil = "^2.8.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Any, ForwardRef, Union

from end_to_end_tests.functional_tests.helpers import (
assert_model_decode_encode,
assert_model_property_type_hint,
Expand Down Expand Up @@ -37,7 +35,6 @@
".models.ModelWithArrayOfInts",
".models.ModelWithArrayOfObjects",
".models.SimpleObject",
".types.Unset",
)
class TestArraySchemas:
def test_array_of_any(self, ModelWithArrayOfAny):
Expand All @@ -53,9 +50,6 @@ def test_array_of_int(self, ModelWithArrayOfInts):
{"arrayProp": [1, 2]},
ModelWithArrayOfInts(array_prop=[1, 2]),
)
# Note, currently arrays of simple types are not validated, so the following assertion would fail:
# with pytest.raises(TypeError):
# ModelWithArrayOfInt.from_dict({"arrayProp": [1, "a"]})

def test_array_of_object(self, ModelWithArrayOfObjects, SimpleObject):
assert_model_decode_encode(
Expand All @@ -64,10 +58,10 @@ def test_array_of_object(self, ModelWithArrayOfObjects, SimpleObject):
ModelWithArrayOfObjects(array_prop=[SimpleObject(name="a"), SimpleObject(name="b")]),
)

def test_type_hints(self, ModelWithArrayOfAny, ModelWithArrayOfInts, ModelWithArrayOfObjects, Unset):
assert_model_property_type_hint(ModelWithArrayOfAny, "array_prop", Union[list[Any], Unset])
assert_model_property_type_hint(ModelWithArrayOfInts, "array_prop", Union[list[int], Unset])
assert_model_property_type_hint(ModelWithArrayOfObjects, "array_prop", Union[list["SimpleObject"], Unset])
def test_type_hints(self, ModelWithArrayOfAny, ModelWithArrayOfInts, ModelWithArrayOfObjects):
assert_model_property_type_hint(ModelWithArrayOfAny, "array_prop", "list[Any] | Unset")
assert_model_property_type_hint(ModelWithArrayOfInts, "array_prop", "list[int] | Unset")
assert_model_property_type_hint(ModelWithArrayOfObjects, "array_prop", "list[SimpleObject] | Unset")


@with_generated_client_fixture(
Expand Down Expand Up @@ -108,7 +102,6 @@ def test_type_hints(self, ModelWithArrayOfAny, ModelWithArrayOfInts, ModelWithAr
".models.ModelWithPrefixItems",
".models.ModelWithMixedItems",
".models.SimpleObject",
".types.Unset",
)
class TestArraysWithPrefixItems:
def test_single_prefix_item(self, ModelWithSinglePrefixItem):
Expand All @@ -132,17 +125,17 @@ def test_prefix_items_and_regular_items(self, ModelWithMixedItems, SimpleObject)
ModelWithMixedItems(array_prop=[SimpleObject(name="a"), "b"]),
)

def test_type_hints(self, ModelWithSinglePrefixItem, ModelWithPrefixItems, ModelWithMixedItems, Unset):
assert_model_property_type_hint(ModelWithSinglePrefixItem, "array_prop", Union[list[str], Unset])
def test_type_hints(self, ModelWithSinglePrefixItem, ModelWithPrefixItems, ModelWithMixedItems):
assert_model_property_type_hint(ModelWithSinglePrefixItem, "array_prop", "list[str] | Unset")
assert_model_property_type_hint(
ModelWithPrefixItems,
"array_prop",
Union[list[Union[ForwardRef("SimpleObject"), str]], Unset],
"list[SimpleObject | str] | Unset",
)
assert_model_property_type_hint(
ModelWithMixedItems,
"array_prop",
Union[list[Union[ForwardRef("SimpleObject"), str]], Unset],
"list[SimpleObject | str] | Unset",
)
# Note, this test is asserting the current behavior which, due to limitations of the implementation
# (see: https://github.com/openapi-generators/openapi-python-client/pull/1130), is not really doing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def test_model_description(self, MyModel):
def test_model_properties(self, MyModel):
assert set(DocstringParser(MyModel).get_section("Attributes:")) == {
"req_str (str): This is necessary.",
"opt_str (Union[Unset, str]): This isn't necessary.",
"opt_str (str | Unset): This isn't necessary.",
"undescribed_prop (str):",
}

Expand Down Expand Up @@ -146,8 +146,8 @@ def test_response_single_type(self, get_simple_thing_sync):
def test_response_union_type(self, post_simple_thing_sync):
returns_line = DocstringParser(post_simple_thing_sync).get_section("Returns:")[0]
assert returns_line in (
"Union[GoodResponse, ErrorResponse]",
"Union[ErrorResponse, GoodResponse]",
"GoodResponse | ErrorResponse",
"ErrorResponse | GoodResponse",
)

def test_request_body(self, post_simple_thing_sync):
Expand All @@ -159,5 +159,5 @@ def test_params(self, get_attribute_by_index_sync):
assert DocstringParser(get_attribute_by_index_sync).get_section("Args:") == [
"id (str): Which one.",
"index (int):",
"fries (Union[Unset, bool]): Do you want fries with that?",
"fries (bool | Unset): Do you want fries with that?",
]
Loading