diff --git a/CHANGELOG.md b/CHANGELOG.md index e2825976..47023884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.12.0] + +### Changed + +- Dropped Python3.8 support while it has reached EOL. ([]) + +## [1.11.1] + +### Fixed +- Kafka `conversion` marshaller and unmarshaller typings ([#240]) +- Improved public API type annotations and fixed unit test type errors ([#248]) + ## [1.11.0] ### Fixed @@ -287,3 +299,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#232]: https://github.com/cloudevents/sdk-python/pull/232 [#235]: https://github.com/cloudevents/sdk-python/pull/235 [#236]: https://github.com/cloudevents/sdk-python/pull/236 +[#240]: https://github.com/cloudevents/sdk-python/pull/240 +[#248]: https://github.com/cloudevents/sdk-python/pull/248 diff --git a/samples/http-image-cloudevents/client.py b/samples/http-image-cloudevents/client.py index a61303f1..2baf8926 100644 --- a/samples/http-image-cloudevents/client.py +++ b/samples/http-image-cloudevents/client.py @@ -24,7 +24,7 @@ image_bytes = resp.content -def send_binary_cloud_event(url: str): +def send_binary_cloud_event(url: str) -> None: # Create cloudevent attributes = { "type": "com.example.string", @@ -41,7 +41,7 @@ def send_binary_cloud_event(url: str): print(f"Sent {event['id']} of type {event['type']}") -def send_structured_cloud_event(url: str): +def send_structured_cloud_event(url: str) -> None: # Create cloudevent attributes = { "type": "com.example.base64", diff --git a/src/cloudevents/v1/__init__.py b/src/cloudevents/v1/__init__.py index 1f52fdbb..e97372bc 100644 --- a/src/cloudevents/v1/__init__.py +++ b/src/cloudevents/v1/__init__.py @@ -12,4 +12,4 @@ # License for the specific language governing permissions and limitations # under the License. -__version__ = "1.11.0" +__version__ = "1.12.0" diff --git a/src/cloudevents/v1/abstract/event.py b/src/cloudevents/v1/abstract/event.py index c18ca34b..18c6df19 100644 --- a/src/cloudevents/v1/abstract/event.py +++ b/src/cloudevents/v1/abstract/event.py @@ -32,7 +32,7 @@ class CloudEvent: @classmethod def create( cls: typing.Type[AnyCloudEvent], - attributes: typing.Dict[str, typing.Any], + attributes: typing.Mapping[str, typing.Any], data: typing.Optional[typing.Any], ) -> AnyCloudEvent: """ diff --git a/src/cloudevents/v1/conversion.py b/src/cloudevents/v1/conversion.py index 3d9899a2..17603d32 100644 --- a/src/cloudevents/v1/conversion.py +++ b/src/cloudevents/v1/conversion.py @@ -91,7 +91,9 @@ def from_json( def from_http( event_type: typing.Type[AnyCloudEvent], - headers: typing.Mapping[str, str], + headers: typing.Union[ + typing.Mapping[str, str], types.SupportsDuplicateItems[str, str] + ], data: typing.Optional[typing.Union[str, bytes]], data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, ) -> AnyCloudEvent: @@ -260,7 +262,7 @@ def best_effort_encode_attribute_value(value: typing.Any) -> typing.Any: def from_dict( event_type: typing.Type[AnyCloudEvent], - event: typing.Dict[str, typing.Any], + event: typing.Mapping[str, typing.Any], ) -> AnyCloudEvent: """ Constructs an Event object of a given `event_type` from diff --git a/src/cloudevents/v1/http/conversion.py b/src/cloudevents/v1/http/conversion.py index 050eb25c..8b1cea42 100644 --- a/src/cloudevents/v1/http/conversion.py +++ b/src/cloudevents/v1/http/conversion.py @@ -37,7 +37,9 @@ def from_json( def from_http( - headers: typing.Dict[str, str], + headers: typing.Union[ + typing.Mapping[str, str], types.SupportsDuplicateItems[str, str] + ], data: typing.Optional[typing.Union[str, bytes]], data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, ) -> CloudEvent: @@ -58,7 +60,7 @@ def from_http( def from_dict( - event: typing.Dict[str, typing.Any], + event: typing.Mapping[str, typing.Any], ) -> CloudEvent: """ Constructs a CloudEvent from a dict `event` representation. diff --git a/src/cloudevents/v1/http/event.py b/src/cloudevents/v1/http/event.py index 69f38110..ccdd7ebd 100644 --- a/src/cloudevents/v1/http/event.py +++ b/src/cloudevents/v1/http/event.py @@ -34,11 +34,13 @@ class CloudEvent(abstract.CloudEvent): @classmethod def create( - cls, attributes: typing.Dict[str, typing.Any], data: typing.Optional[typing.Any] + cls, + attributes: typing.Mapping[str, typing.Any], + data: typing.Optional[typing.Any], ) -> "CloudEvent": return cls(attributes, data) - def __init__(self, attributes: typing.Dict[str, str], data: typing.Any = None): + def __init__(self, attributes: typing.Mapping[str, str], data: typing.Any = None): """ Event Constructor :param attributes: a dict with cloudevent attributes. Minimally diff --git a/src/cloudevents/v1/kafka/conversion.py b/src/cloudevents/v1/kafka/conversion.py index 3c0d3c7e..04a3ab74 100644 --- a/src/cloudevents/v1/kafka/conversion.py +++ b/src/cloudevents/v1/kafka/conversion.py @@ -21,9 +21,14 @@ from cloudevents_v1.kafka.exceptions import KeyMapperError from cloudevents_v1.sdk import types -DEFAULT_MARSHALLER: types.MarshallerType = json.dumps -DEFAULT_UNMARSHALLER: types.MarshallerType = json.loads -DEFAULT_EMBEDDED_DATA_MARSHALLER: types.MarshallerType = lambda x: x +JSON_MARSHALLER: types.MarshallerType = json.dumps +JSON_UNMARSHALLER: types.UnmarshallerType = json.loads +IDENTITY_MARSHALLER = IDENTITY_UNMARSHALLER = lambda x: x + +DEFAULT_MARSHALLER: types.MarshallerType = JSON_MARSHALLER +DEFAULT_UNMARSHALLER: types.UnmarshallerType = JSON_UNMARSHALLER +DEFAULT_EMBEDDED_DATA_MARSHALLER: types.MarshallerType = IDENTITY_MARSHALLER +DEFAULT_EMBEDDED_DATA_UNMARSHALLER: types.UnmarshallerType = IDENTITY_UNMARSHALLER class KafkaMessage(typing.NamedTuple): @@ -106,11 +111,29 @@ def to_binary( return KafkaMessage(headers, message_key, data) +@typing.overload def from_binary( message: KafkaMessage, - event_type: typing.Optional[typing.Type[AnyCloudEvent]] = None, - data_unmarshaller: typing.Optional[types.MarshallerType] = None, + event_type: None = None, + data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, +) -> http.CloudEvent: + pass + + +@typing.overload +def from_binary( + message: KafkaMessage, + event_type: typing.Type[AnyCloudEvent], + data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, ) -> AnyCloudEvent: + pass + + +def from_binary( + message: KafkaMessage, + event_type: typing.Optional[typing.Type[AnyCloudEvent]] = None, + data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, +) -> typing.Union[http.CloudEvent, AnyCloudEvent]: """ Returns a CloudEvent from a KafkaMessage in binary format. @@ -139,10 +162,11 @@ def from_binary( raise cloud_exceptions.DataUnmarshallerError( f"Failed to unmarshall data with error: {type(e).__name__}('{e}')" ) + result: typing.Union[http.CloudEvent, AnyCloudEvent] if event_type: result = event_type.create(attributes, data) else: - result = http.CloudEvent.create(attributes, data) # type: ignore + result = http.CloudEvent.create(attributes, data) return result @@ -205,12 +229,32 @@ def to_structured( return KafkaMessage(headers, message_key, value) +@typing.overload def from_structured( message: KafkaMessage, - event_type: typing.Optional[typing.Type[AnyCloudEvent]] = None, - data_unmarshaller: typing.Optional[types.MarshallerType] = None, + event_type: None = None, + data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, + envelope_unmarshaller: typing.Optional[types.UnmarshallerType] = None, +) -> http.CloudEvent: + pass + + +@typing.overload +def from_structured( + message: KafkaMessage, + event_type: typing.Type[AnyCloudEvent], + data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, envelope_unmarshaller: typing.Optional[types.UnmarshallerType] = None, ) -> AnyCloudEvent: + pass + + +def from_structured( + message: KafkaMessage, + event_type: typing.Optional[typing.Type[AnyCloudEvent]] = None, + data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, + envelope_unmarshaller: typing.Optional[types.UnmarshallerType] = None, +) -> typing.Union[http.CloudEvent, AnyCloudEvent]: """ Returns a CloudEvent from a KafkaMessage in structured format. @@ -222,7 +266,7 @@ def from_structured( :returns: CloudEvent """ - data_unmarshaller = data_unmarshaller or DEFAULT_EMBEDDED_DATA_MARSHALLER + data_unmarshaller = data_unmarshaller or DEFAULT_EMBEDDED_DATA_UNMARSHALLER envelope_unmarshaller = envelope_unmarshaller or DEFAULT_UNMARSHALLER try: structure = envelope_unmarshaller(message.value) @@ -259,8 +303,9 @@ def from_structured( attributes["datacontenttype"] = val.decode() else: attributes[header.lower()] = val.decode() + result: typing.Union[AnyCloudEvent, http.CloudEvent] if event_type: result = event_type.create(attributes, data) else: - result = http.CloudEvent.create(attributes, data) # type: ignore + result = http.CloudEvent.create(attributes, data) return result diff --git a/src/cloudevents/v1/pydantic/v1/conversion.py b/src/cloudevents/v1/pydantic/v1/conversion.py index efd7a7f4..f00673d5 100644 --- a/src/cloudevents/v1/pydantic/v1/conversion.py +++ b/src/cloudevents/v1/pydantic/v1/conversion.py @@ -21,7 +21,9 @@ def from_http( - headers: typing.Dict[str, str], + headers: typing.Union[ + typing.Mapping[str, str], types.SupportsDuplicateItems[str, str] + ], data: typing.Optional[typing.AnyStr], data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, ) -> CloudEvent: @@ -63,7 +65,7 @@ def from_json( def from_dict( - event: typing.Dict[str, typing.Any], + event: typing.Mapping[str, typing.Any], ) -> CloudEvent: """ Construct an CloudEvent from a dict `event` representation. diff --git a/src/cloudevents/v1/pydantic/v1/event.py b/src/cloudevents/v1/pydantic/v1/event.py index 999828b3..e272aa7d 100644 --- a/src/cloudevents/v1/pydantic/v1/event.py +++ b/src/cloudevents/v1/pydantic/v1/event.py @@ -102,7 +102,9 @@ class CloudEvent(abstract.CloudEvent, BaseModel): # type: ignore @classmethod def create( - cls, attributes: typing.Dict[str, typing.Any], data: typing.Optional[typing.Any] + cls, + attributes: typing.Mapping[str, typing.Any], + data: typing.Optional[typing.Any], ) -> "CloudEvent": return cls(attributes, data) @@ -157,7 +159,7 @@ def create( def __init__( # type: ignore[no-untyped-def] self, - attributes: typing.Optional[typing.Dict[str, typing.Any]] = None, + attributes: typing.Optional[typing.Mapping[str, typing.Any]] = None, data: typing.Optional[typing.Any] = None, **kwargs, ): diff --git a/src/cloudevents/v1/pydantic/v2/conversion.py b/src/cloudevents/v1/pydantic/v2/conversion.py index a164091b..0e288661 100644 --- a/src/cloudevents/v1/pydantic/v2/conversion.py +++ b/src/cloudevents/v1/pydantic/v2/conversion.py @@ -22,7 +22,9 @@ def from_http( - headers: typing.Dict[str, str], + headers: typing.Union[ + typing.Mapping[str, str], types.SupportsDuplicateItems[str, str] + ], data: typing.Optional[typing.AnyStr], data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, ) -> CloudEvent: @@ -64,7 +66,7 @@ def from_json( def from_dict( - event: typing.Dict[str, typing.Any], + event: typing.Mapping[str, typing.Any], ) -> CloudEvent: """ Construct an CloudEvent from a dict `event` representation. diff --git a/src/cloudevents/v1/pydantic/v2/event.py b/src/cloudevents/v1/pydantic/v2/event.py index 26c2fcb9..81ec6bd5 100644 --- a/src/cloudevents/v1/pydantic/v2/event.py +++ b/src/cloudevents/v1/pydantic/v2/event.py @@ -43,7 +43,9 @@ class CloudEvent(abstract.CloudEvent, BaseModel): # type: ignore @classmethod def create( - cls, attributes: typing.Dict[str, typing.Any], data: typing.Optional[typing.Any] + cls, + attributes: typing.Mapping[str, typing.Any], + data: typing.Optional[typing.Any], ) -> "CloudEvent": return cls(attributes, data) @@ -102,7 +104,7 @@ def create( def __init__( # type: ignore[no-untyped-def] self, - attributes: typing.Optional[typing.Dict[str, typing.Any]] = None, + attributes: typing.Optional[typing.Mapping[str, typing.Any]] = None, data: typing.Optional[typing.Any] = None, **kwargs, ): @@ -172,6 +174,8 @@ def model_validate_json( *, strict: typing.Optional[bool] = None, context: typing.Optional[typing.Dict[str, Any]] = None, + by_alias: typing.Optional[bool] = None, + by_name: typing.Optional[bool] = None, ) -> "CloudEvent": return conversion.from_json(cls, json_data) diff --git a/src/cloudevents/v1/sdk/event/v1.py b/src/cloudevents/v1/sdk/event/v1.py index dfa470d1..f99c9236 100644 --- a/src/cloudevents/v1/sdk/event/v1.py +++ b/src/cloudevents/v1/sdk/event/v1.py @@ -11,10 +11,15 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from __future__ import annotations + import typing from cloudevents_v1.sdk.event import base, opt +if typing.TYPE_CHECKING: + from typing_extensions import Self + class Event(base.BaseEvent): _ce_required_fields = {"id", "source", "type", "specversion"} @@ -79,39 +84,39 @@ def Extensions(self) -> dict: return {} return dict(result) - def SetEventType(self, eventType: str) -> base.BaseEvent: + def SetEventType(self, eventType: str) -> Self: self.Set("type", eventType) return self - def SetSource(self, source: str) -> base.BaseEvent: + def SetSource(self, source: str) -> Self: self.Set("source", source) return self - def SetEventID(self, eventID: str) -> base.BaseEvent: + def SetEventID(self, eventID: str) -> Self: self.Set("id", eventID) return self - def SetEventTime(self, eventTime: typing.Optional[str]) -> base.BaseEvent: + def SetEventTime(self, eventTime: typing.Optional[str]) -> Self: self.Set("time", eventTime) return self - def SetSubject(self, subject: typing.Optional[str]) -> base.BaseEvent: + def SetSubject(self, subject: typing.Optional[str]) -> Self: self.Set("subject", subject) return self - def SetSchema(self, schema: typing.Optional[str]) -> base.BaseEvent: + def SetSchema(self, schema: typing.Optional[str]) -> Self: self.Set("dataschema", schema) return self - def SetContentType(self, contentType: typing.Optional[str]) -> base.BaseEvent: + def SetContentType(self, contentType: typing.Optional[str]) -> Self: self.Set("datacontenttype", contentType) return self - def SetData(self, data: typing.Optional[object]) -> base.BaseEvent: + def SetData(self, data: typing.Optional[object]) -> Self: self.Set("data", data) return self - def SetExtensions(self, extensions: typing.Optional[dict]) -> base.BaseEvent: + def SetExtensions(self, extensions: typing.Optional[dict]) -> Self: self.Set("extensions", extensions) return self diff --git a/src/cloudevents/v1/sdk/types.py b/src/cloudevents/v1/sdk/types.py index e6ab46e4..6baef6b0 100644 --- a/src/cloudevents/v1/sdk/types.py +++ b/src/cloudevents/v1/sdk/types.py @@ -14,9 +14,25 @@ import typing +_K_co = typing.TypeVar("_K_co", covariant=True) +_V_co = typing.TypeVar("_V_co", covariant=True) + # Use consistent types for marshal and unmarshal functions across # both JSON and Binary format. MarshallerType = typing.Callable[[typing.Any], typing.AnyStr] UnmarshallerType = typing.Callable[[typing.AnyStr], typing.Any] + + +class SupportsDuplicateItems(typing.Protocol[_K_co, _V_co]): + """ + Dict-like objects with an items() method that may produce duplicate keys. + """ + + # This is wider than _typeshed.SupportsItems, which expects items() to + # return type an AbstractSet. werkzeug's Headers class satisfies this type, + # but not _typeshed.SupportsItems. + + def items(self) -> typing.Iterable[typing.Tuple[_K_co, _V_co]]: + pass diff --git a/src/cloudevents/v1/tests/test_converters.py b/src/cloudevents/v1/tests/test_converters.py index f9940409..ef3f276b 100644 --- a/src/cloudevents/v1/tests/test_converters.py +++ b/src/cloudevents/v1/tests/test_converters.py @@ -20,7 +20,7 @@ def test_binary_converter_raise_unsupported(): with pytest.raises(exceptions.UnsupportedEvent): cnvtr = binary.BinaryHTTPCloudEventConverter() - cnvtr.read(None, {}, None, None) + cnvtr.read(None, {}, None, None) # type: ignore[arg-type] # intentionally wrong type # noqa: E501 def test_base_converters_raise_exceptions(): @@ -34,8 +34,8 @@ def test_base_converters_raise_exceptions(): with pytest.raises(Exception): cnvtr = base.Converter() - cnvtr.write(None, None) + cnvtr.write(None, None) # type: ignore[arg-type] # intentionally wrong type with pytest.raises(Exception): cnvtr = base.Converter() - cnvtr.read(None, None, None, None) + cnvtr.read(None, None, None, None) # type: ignore[arg-type] # intentionally wrong type # noqa: E501 diff --git a/src/cloudevents/v1/tests/test_event_from_request_converter.py b/src/cloudevents/v1/tests/test_event_from_request_converter.py index 2f98a640..5f532902 100644 --- a/src/cloudevents/v1/tests/test_event_from_request_converter.py +++ b/src/cloudevents/v1/tests/test_event_from_request_converter.py @@ -24,7 +24,7 @@ @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) def test_binary_converter_upstream(event_class): m = marshaller.NewHTTPMarshaller([binary.NewBinaryHTTPCloudEventConverter()]) - event = m.FromRequest(event_class(), data.headers[event_class], None, lambda x: x) + event = m.FromRequest(event_class(), data.headers[event_class], b"", lambda x: x) assert event is not None assert event.EventType() == data.ce_type assert event.EventID() == data.ce_id diff --git a/src/cloudevents/v1/tests/test_event_pipeline.py b/src/cloudevents/v1/tests/test_event_pipeline.py index fdb547d5..edbd482a 100644 --- a/src/cloudevents/v1/tests/test_event_pipeline.py +++ b/src/cloudevents/v1/tests/test_event_pipeline.py @@ -76,7 +76,7 @@ def test_object_event_v1(): _, structured_body = m.ToRequest(event) assert isinstance(structured_body, bytes) structured_obj = json.loads(structured_body) - error_msg = f"Body was {structured_body}, obj is {structured_obj}" + error_msg = f"Body was {structured_body!r}, obj is {structured_obj}" assert isinstance(structured_obj, dict), error_msg assert isinstance(structured_obj["data"], dict), error_msg assert len(structured_obj["data"]) == 1, error_msg diff --git a/src/cloudevents/v1/tests/test_http_events.py b/src/cloudevents/v1/tests/test_http_events.py index 6956df2a..1c45f5d1 100644 --- a/src/cloudevents/v1/tests/test_http_events.py +++ b/src/cloudevents/v1/tests/test_http_events.py @@ -11,6 +11,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from __future__ import annotations import bz2 import io @@ -240,11 +241,11 @@ def test_structured_to_request(specversion): assert headers["content-type"] == "application/cloudevents+json" for key in attributes: assert body[key] == attributes[key] - assert body["data"] == data, f"|{body_bytes}|| {body}" + assert body["data"] == data, f"|{body_bytes!r}|| {body}" @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) -def test_attributes_view_accessor(specversion: str): +def test_attributes_view_accessor(specversion: str) -> None: attributes: dict[str, typing.Any] = { "specversion": specversion, "type": "word.found.name", @@ -332,7 +333,7 @@ def test_valid_structured_events(specversion): events_queue = [] num_cloudevents = 30 for i in range(num_cloudevents): - event = { + raw_event = { "id": f"id{i}", "source": f"source{i}.com.test", "type": "cloudevent.test.type", @@ -342,7 +343,7 @@ def test_valid_structured_events(specversion): events_queue.append( from_http( {"content-type": "application/cloudevents+json"}, - json.dumps(event), + json.dumps(raw_event), ) ) @@ -453,7 +454,7 @@ def test_invalid_data_format_structured_from_http(): headers = {"Content-Type": "application/cloudevents+json"} data = 20 with pytest.raises(cloud_exceptions.InvalidStructuredJSON) as e: - from_http(headers, data) + from_http(headers, data) # type: ignore[arg-type] # intentionally wrong type assert "Expected json of type (str, bytes, bytearray)" in str(e.value) @@ -525,7 +526,7 @@ def test_generic_exception(): e.errisinstance(cloud_exceptions.MissingRequiredFields) with pytest.raises(cloud_exceptions.GenericException) as e: - from_http({}, 123) + from_http({}, 123) # type: ignore[arg-type] # intentionally wrong type e.errisinstance(cloud_exceptions.InvalidStructuredJSON) with pytest.raises(cloud_exceptions.GenericException) as e: diff --git a/src/cloudevents/v1/tests/test_marshaller.py b/src/cloudevents/v1/tests/test_marshaller.py index d3ba81a7..1e3ce516 100644 --- a/src/cloudevents/v1/tests/test_marshaller.py +++ b/src/cloudevents/v1/tests/test_marshaller.py @@ -49,14 +49,17 @@ def test_from_request_wrong_unmarshaller(): with pytest.raises(exceptions.InvalidDataUnmarshaller): m = marshaller.NewDefaultHTTPMarshaller() _ = m.FromRequest( - event=v1.Event(), headers={}, body="", data_unmarshaller=object() + event=v1.Event(), + headers={}, + body="", + data_unmarshaller=object(), # type: ignore[arg-type] # intentionally wrong type # noqa: E501 ) def test_to_request_wrong_marshaller(): with pytest.raises(exceptions.InvalidDataMarshaller): m = marshaller.NewDefaultHTTPMarshaller() - _ = m.ToRequest(v1.Event(), data_marshaller="") + _ = m.ToRequest(v1.Event(), data_marshaller="") # type: ignore[arg-type] # intentionally wrong type # noqa: E501 def test_from_request_cannot_read(binary_headers): diff --git a/tox.ini b/tox.ini index 0436a1be..4ef33036 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{38,39,310,311,312},lint +envlist = py{310,311,312,313,314},lint,mypy,mypy-samples-{image,json} skipsdist = True [testenv] @@ -12,7 +12,7 @@ setenv = commands = pytest {env:PYTESTARGS} {posargs} [testenv:reformat] -basepython = python3.11 +basepython = python3.12 deps = black isort @@ -21,7 +21,7 @@ commands = isort cloudevents samples [testenv:lint] -basepython = python3.11 +basepython = python3.12 deps = black isort @@ -30,3 +30,21 @@ commands = black --check . isort -c cloudevents samples flake8 cloudevents samples --ignore W503,E731 --extend-ignore E203 --max-line-length 88 + +[testenv:mypy] +basepython = python3.12 +deps = + -r{toxinidir}/requirements/mypy.txt + # mypy needs test dependencies to check test modules + -r{toxinidir}/requirements/test.txt +commands = mypy cloudevents + +[testenv:mypy-samples-{image,json}] +basepython = python3.12 +setenv = + mypy-samples-image: SAMPLE_DIR={toxinidir}/samples/http-image-cloudevents + mypy-samples-json: SAMPLE_DIR={toxinidir}/samples/http-json-cloudevents +deps = + -r{toxinidir}/requirements/mypy.txt + -r{env:SAMPLE_DIR}/requirements.txt +commands = mypy {env:SAMPLE_DIR}