From c17a81cf4cf4f499c7ce6a0cbf402f60d0a49512 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 1 Jul 2024 14:58:20 +0200 Subject: [PATCH] Generate schema-based deserializers This adds code-generated deserializers based on schemas. It also fixes some member filtering that was previously added but not working properly. --- .../smithy/python/codegen/SetupGenerator.java | 1 + .../python/codegen/StructureGenerator.java | 69 +++++- .../python/codegen/SymbolProperties.java | 6 + .../smithy/python/codegen/SymbolVisitor.java | 19 +- .../smithy/python/codegen/UnionGenerator.java | 62 ++++- .../codegen/generators/ListGenerator.java | 36 +++ .../codegen/generators/MapGenerator.java | 34 +++ .../MemberDeserializerGenerator.java | 233 ++++++++++++++++++ .../smithy-core/tests/unit/test_documents.py | 4 +- 9 files changed, 452 insertions(+), 12 deletions(-) create mode 100644 codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/MemberDeserializerGenerator.java diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SetupGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SetupGenerator.java index 5118eccb..eb014f0c 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SetupGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SetupGenerator.java @@ -111,6 +111,7 @@ private static void writePyproject( reportUnusedFunction = false reportUnusedVariable = false reportUnnecessaryComparison = false + reportUnusedClass = false [tool.black] target-version = ["py311"] diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/StructureGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/StructureGenerator.java index d1393ae6..a87655d2 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/StructureGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/StructureGenerator.java @@ -20,13 +20,16 @@ import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.logging.Logger; import java.util.stream.Collectors; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.HttpBinding; import software.amazon.smithy.model.knowledge.HttpBindingIndex; import software.amazon.smithy.model.knowledge.NullableIndex; +import software.amazon.smithy.model.knowledge.OperationIndex; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.Shape; @@ -37,6 +40,7 @@ import software.amazon.smithy.model.traits.InputTrait; import software.amazon.smithy.model.traits.OutputTrait; import software.amazon.smithy.model.traits.SensitiveTrait; +import software.amazon.smithy.python.codegen.generators.MemberDeserializerGenerator; import software.amazon.smithy.python.codegen.generators.MemberSerializerGenerator; @@ -109,10 +113,13 @@ class $L: ${C|} + ${C|} + """, symbol.getName(), writer.consumer(w -> writeClassDocs(false)), writer.consumer(w -> writeProperties()), - writer.consumer(w -> generateSerializeMethod())); + writer.consumer(w -> generateSerializeMethod()), + writer.consumer(w -> generateDeserializeMethod())); } private void renderError() { @@ -334,20 +341,31 @@ def serialize(self, serializer: ShapeSerializer): private List filterMembers() { var httpIndex = HttpBindingIndex.of(model); + var operationIndex = OperationIndex.of(model); if (shape.hasTrait(InputTrait.class)) { - var bindings = httpIndex.getRequestBindings(shape).keySet(); + var operation = operationIndex.getInputBindings(shape).iterator().next(); + var bindings = httpIndex.getRequestBindings(operation); return shape.members().stream() - .filter(member -> bindings.contains(member.getMemberName())) + .filter(member -> filterMember(member, bindings)) .toList(); } else if (shape.hasTrait(OutputTrait.class)) { - var bindings = httpIndex.getResponseBindings(shape).keySet(); + var operation = operationIndex.getOutputBindings(shape).iterator().next(); + var bindings = httpIndex.getResponseBindings(operation); return shape.members().stream() - .filter(member -> bindings.contains(member.getMemberName())) + .filter(member -> filterMember(member, bindings)) .toList(); } return shape.members().stream().toList(); } + private boolean filterMember(MemberShape member, Map bindings) { + if (bindings.containsKey(member.getMemberName())) { + var binding = bindings.get(member.getMemberName()); + return binding.getLocation() == HttpBinding.Location.DOCUMENT; + } + return true; + } + private boolean isNullable(MemberShape member) { if (!NullableIndex.of(model).isMemberNullable(member)) { return false; @@ -356,4 +374,45 @@ private boolean isNullable(MemberShape member) { return !target.isDocumentShape() || member.getMemberTrait(model, DefaultTrait.class).isEmpty(); } + private void generateDeserializeMethod() { + writer.pushState(); + writer.addLogger(); + writer.addStdlibImports("typing", Set.of("Self", "Any")); + writer.addImport("smithy_core.deserializers", "ShapeDeserializer"); + + var deserializeableMembers = filterMembers(); + var schemaSymbol = symbolProvider.toSymbol(shape).expectProperty(SymbolProperties.SCHEMA); + writer.putContext("schema", schemaSymbol); + writer.write(""" + @classmethod + def deserialize(cls, deserializer: ShapeDeserializer) -> Self: + kwargs: dict[str, Any] = {} + + def _consumer(schema: Schema, de: ShapeDeserializer) -> None: + match schema.expect_member_index(): + ${C|} + case _: + logger.debug(f"Unexpected member schema: {schema}") + + deserializer.read_struct($T, consumer=_consumer) + return cls(**kwargs) + + """, + writer.consumer(w -> deserializeMembers(deserializeableMembers)), + schemaSymbol); + writer.popState(); + } + + private void deserializeMembers(List members) { + int index = 0; + for (MemberShape member : members) { + var target = model.expectShape(member.getTarget()); + writer.write(""" + case $L: + kwargs[$S] = ${C|} + """, index++, symbolProvider.toMemberName(member), writer.consumer(w -> + target.accept(new MemberDeserializerGenerator(context, writer, member, "de")) + )); + } + } } diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SymbolProperties.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SymbolProperties.java index dd1e5c67..31bfb797 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SymbolProperties.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SymbolProperties.java @@ -68,5 +68,11 @@ public final class SymbolProperties { */ public static final Property SERIALIZER = Property.named("serializer"); + /** + * Contains a symbol pointing to the shape's deserializer method. This is only used for + * lists, maps, and unions. + */ + public static final Property DESERIALIZER = Property.named("deserializer"); + private SymbolProperties() {} } diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SymbolVisitor.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SymbolVisitor.java index 4df4e649..ad6c64a0 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SymbolVisitor.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/SymbolVisitor.java @@ -191,6 +191,10 @@ public Symbol listShape(ListShape shape) { shape, "_serialize_" + CaseUtils.toSnakeCase(shape.getId().getName()), SHAPES_FILE, false) .build()); + builder.putProperty(SymbolProperties.DESERIALIZER, createGeneratedSymbolBuilder( + shape, "_deserialize_" + CaseUtils.toSnakeCase(shape.getId().getName()), SHAPES_FILE, false) + .build()); + return builder.build(); } @@ -206,6 +210,10 @@ public Symbol mapShape(MapShape shape) { shape, "_serialize_" + CaseUtils.toSnakeCase(shape.getId().getName()), SHAPES_FILE, false) .build()); + builder.putProperty(SymbolProperties.DESERIALIZER, createGeneratedSymbolBuilder( + shape, "_deserialize_" + CaseUtils.toSnakeCase(shape.getId().getName()), SHAPES_FILE, false) + .build()); + return builder.build(); } @@ -332,9 +340,14 @@ public Symbol unionShape(UnionShape shape) { var unknownName = name + "Unknown"; var unknownSymbol = createGeneratedSymbolBuilder(shape, unknownName, SHAPES_FILE).build(); - return createGeneratedSymbolBuilder(shape, name, SHAPES_FILE) - .putProperty(SymbolProperties.UNION_UNKNOWN, unknownSymbol) - .build(); + var builder = createGeneratedSymbolBuilder(shape, name, SHAPES_FILE) + .putProperty(SymbolProperties.UNION_UNKNOWN, unknownSymbol); + + builder.putProperty(SymbolProperties.DESERIALIZER, createGeneratedSymbolBuilder( + shape, "_" + name + "Deserializer", SHAPES_FILE, false) + .build()); + + return builder.build(); } @Override diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/UnionGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/UnionGenerator.java index f42eba14..19b25ab9 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/UnionGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/UnionGenerator.java @@ -24,6 +24,7 @@ import software.amazon.smithy.model.shapes.UnionShape; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.StringTrait; +import software.amazon.smithy.python.codegen.generators.MemberDeserializerGenerator; import software.amazon.smithy.python.codegen.generators.MemberSerializerGenerator; /** @@ -54,6 +55,7 @@ final class UnionGenerator implements Runnable { @Override public void run() { + writer.pushState(); var parentName = symbolProvider.toSymbol(shape).getName(); writer.addStdlibImport("dataclasses", "dataclass"); writer.addImport("smithy_core.serializers", "ShapeSerializer"); @@ -120,8 +122,64 @@ raise SmithyException("Unknown union variants may not be serialized.") memberNames.add(unknownSymbol.getName()); shape.getTrait(DocumentationTrait.class).ifPresent(trait -> writer.writeComment(trait.getValue())); - writer.addStdlibImport("typing", "Union"); - writer.write("$L = Union[$L]", parentName, String.join(", ", memberNames)); + writer.write("type $L = $L\n", parentName, String.join(" | ", memberNames)); + generateDeserializer(); + writer.popState(); + } + + private void generateDeserializer() { + writer.addLogger(); + writer.addStdlibImports("typing", Set.of("Self", "Any")); + writer.addImport("smithy_core.deserializers", "ShapeDeserializer"); + writer.addImport("smithy_core.exceptions", "SmithyException"); + + // TODO: add in unknown handling + + var symbol = symbolProvider.toSymbol(shape); + var deserializerSymbol = symbol.expectProperty(SymbolProperties.DESERIALIZER); + var schemaSymbol = symbol.expectProperty(SymbolProperties.SCHEMA); + writer.putContext("schema", schemaSymbol); + writer.write(""" + class $1L: + _result: $2T | None = None + + def deserialize(self, deserializer: ShapeDeserializer) -> $2T: + self._result = None + deserializer.read_struct($3T, self._consumer) + + if self._result is None: + raise SmithyException("Unions must have exactly one value, but found none.") + + return self._result + + def _consumer(self, schema: Schema, de: ShapeDeserializer) -> None: + match schema.expect_member_index(): + ${4C|} + case _: + logger.debug(f"Unexpected member schema: {schema}") + + def _set_result(self, value: $2T) -> None: + if self._result is not None: + raise SmithyException("Unions must have exactly one value, but found more than one.") + self._result = value + """, + deserializerSymbol.getName(), + symbol, + schemaSymbol, + writer.consumer(w -> deserializeMembers())); + } + + private void deserializeMembers() { + int index = 0; + for (MemberShape member : shape.members()) { + var target = model.expectShape(member.getTarget()); + writer.write(""" + case $L: + self._set_result($T(${C|})) + """, index++, symbolProvider.toSymbol(member), writer.consumer(w -> + target.accept(new MemberDeserializerGenerator(context, writer, member, "de")) + )); + } } } diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/ListGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/ListGenerator.java index 54fa65fd..72026faf 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/ListGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/ListGenerator.java @@ -30,6 +30,7 @@ public ListGenerator(GenerationContext context, PythonWriter writer, ListShape s @Override public void run() { generateSerializer(); + generateDeserializer(); } private void generateSerializer() { @@ -61,4 +62,39 @@ private void generateSerializer() { new MemberSerializerGenerator(context, w, shape.getMember(), "ls")))); writer.popState(); } + + private void generateDeserializer() { + var listSymbol = context.symbolProvider().toSymbol(shape); + var deserializerSymbol = listSymbol.expectProperty(SymbolProperties.DESERIALIZER); + var memberTarget = context.model().expectShape(shape.getMember().getTarget()); + + writer.pushState(); + writer.addImport("smithy_core.serializers", "ShapeSerializer"); + writer.addImport("smithy_core.schemas", "Schema"); + var sparse = shape.hasTrait(SparseTrait.class); + writer.putContext("sparse", sparse); + writer.putContext("includeSchema", sparse || ( + !memberTarget.isUnionShape() && !memberTarget.isStructureShape())); + writer.write(""" + def $1L(deserializer: ShapeDeserializer, schema: Schema) -> $2T: + result: $2T = [] + ${?includeSchema} + member_schema = schema.members["member"] + ${/includeSchema} + deserializer.read_list( + schema, + ${?sparse} + lambda d: result.append(d.read_optional(member_schema, lambda s: ${3C|})) + ${/sparse} + ${^sparse} + lambda d: result.append(${3C|}) + ${/sparse} + ) + return result + """, deserializerSymbol.getName(), listSymbol, + writer.consumer(w -> memberTarget.accept( + new MemberDeserializerGenerator(context, w, shape.getMember(), "d") + ))); + writer.popState(); + } } diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/MapGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/MapGenerator.java index ee8c74b7..18ee2882 100644 --- a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/MapGenerator.java +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/MapGenerator.java @@ -30,6 +30,7 @@ public MapGenerator(GenerationContext context, PythonWriter writer, MapShape sha @Override public void run() { generateSerializer(); + generateDeserializer(); } private void generateSerializer() { @@ -64,4 +65,37 @@ private void generateSerializer() { new MemberSerializerGenerator(context, w, shape.getValue(), "vs")))); writer.popState(); } + + private void generateDeserializer() { + var listSymbol = context.symbolProvider().toSymbol(shape); + var deserializerSymbol = listSymbol.expectProperty(SymbolProperties.DESERIALIZER); + var valueTarget = context.model().expectShape(shape.getValue().getTarget()); + + writer.pushState(); + writer.addImport("smithy_core.serializers", "ShapeSerializer"); + writer.addImport("smithy_core.schemas", "Schema"); + var sparse = shape.hasTrait(SparseTrait.class); + writer.putContext("sparse", sparse); + writer.putContext("includeSchema", sparse || ( + !valueTarget.isUnionShape() && !valueTarget.isStructureShape())); + writer.write(""" + def $1L(deserializer: ShapeDeserializer, schema: Schema) -> $2T: + result: $2T = {} + value_schema = schema.members["value"] + deserializer.read_map( + schema, + ${?sparse} + lambda k, d: result.__setitem__(k, d.read_optional(value_schema, lambda s: ${3C|})) + ${/sparse} + ${^sparse} + lambda k, d: result.__setitem__(k, ${3C|}) + ${/sparse} + ) + return result + """, deserializerSymbol.getName(), listSymbol, + writer.consumer(w -> valueTarget.accept( + new MemberDeserializerGenerator(context, w, shape.getValue(), "d") + ))); + writer.popState(); + } } diff --git a/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/MemberDeserializerGenerator.java b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/MemberDeserializerGenerator.java new file mode 100644 index 00000000..ccc8d397 --- /dev/null +++ b/codegen/smithy-python-codegen/src/main/java/software/amazon/smithy/python/codegen/generators/MemberDeserializerGenerator.java @@ -0,0 +1,233 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.python.codegen.generators; + +import java.util.Locale; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.model.shapes.BigDecimalShape; +import software.amazon.smithy.model.shapes.BigIntegerShape; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.ByteShape; +import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.LongShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.ShortShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.TimestampShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.python.codegen.GenerationContext; +import software.amazon.smithy.python.codegen.PythonWriter; +import software.amazon.smithy.python.codegen.SymbolProperties; + +/** + * Generates calls to shape serializers for member shapes. + */ +public class MemberDeserializerGenerator extends ShapeVisitor.DataShapeVisitor { + + private final GenerationContext context; + private final PythonWriter writer; + private final MemberShape member; + private final String deserializerName; + + public MemberDeserializerGenerator( + GenerationContext context, + PythonWriter writer, + MemberShape member, + String deserializerName + ) { + this.context = context; + this.writer = writer; + this.member = member; + this.deserializerName = deserializerName; + } + + private void pushMemberState() { + pushMemberState(member); + } + + private void pushMemberState(MemberShape member) { + //writer.pushState(); + writer.putContext("deserializer", deserializerName); + writer.putContext("member", member.getMemberName()); + writer.putContext("isListMember", false); + writer.putContext("isMapMember", false); + var parent = context.model().expectShape(member.getContainer()); + if (parent.isListShape()) { + writer.putContext("isListMember", true); + } else if (parent.isMapShape()) { + writer.putContext("isMapMember", true); + } + } + + private void writeDeserializer(Shape shape) { + writeDeserializer(shape.getType().name().toLowerCase(Locale.ENGLISH)); + } + + private void writeDeserializer(String shapeTypeName) { + pushMemberState(); + writer.write( + "${deserializer:L}.read_$L(${C|})", + shapeTypeName, + writer.consumer(w -> writeSchema()) + ); + //writer.popState(); + } + + private void writeSchema() { + var parent = context.model().expectShape(member.getContainer()); + if (parent.isListShape()) { + writer.writeInline("member_schema"); + } else if (parent.isMapShape()) { + writer.writeInline("value_schema"); + } else { + writer.writeInline("${schema:T}.members[${member:S}]"); + } + } + + @Override + public Void blobShape(BlobShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void booleanShape(BooleanShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void byteShape(ByteShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void shortShape(ShortShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void integerShape(IntegerShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + writeDeserializer("integer"); + return null; + } + + @Override + public Void longShape(LongShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void floatShape(FloatShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void doubleShape(DoubleShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void bigIntegerShape(BigIntegerShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void bigDecimalShape(BigDecimalShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + writeDeserializer("string"); + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + writeDeserializer(shape); + return null; + } + + @Override + public Void memberShape(MemberShape shape) { + throw new CodegenException("Unexpected shape type: Member"); + } + + @Override + public Void listShape(ListShape shape) { + deserializerSymbolShape(shape); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + deserializerSymbolShape(shape); + return null; + } + + private void deserializerSymbolShape(Shape shape) { + pushMemberState(); + var deserializerSymbol = context.symbolProvider().toSymbol(shape) + .expectProperty(SymbolProperties.DESERIALIZER); + writer.write("$T(${deserializer:L}, ${C|})", + deserializerSymbol, writer.consumer(w -> writeSchema())); + //writer.popState(); + } + + @Override + public Void structureShape(StructureShape shape) { + pushMemberState(); + writer.write("$T.deserialize(${deserializer:L})", context.symbolProvider().toSymbol(shape)); + return null; + } + + @Override + public Void unionShape(UnionShape shape) { + pushMemberState(); + var deserializerSymbol = context.symbolProvider().toSymbol(shape) + .expectProperty(SymbolProperties.DESERIALIZER); + writer.write("$T().deserialize(${deserializer:L})", deserializerSymbol); + //writer.popState(); + return null; + } +} diff --git a/python-packages/smithy-core/tests/unit/test_documents.py b/python-packages/smithy-core/tests/unit/test_documents.py index 7c76e723..5535b2c3 100644 --- a/python-packages/smithy-core/tests/unit/test_documents.py +++ b/python-packages/smithy-core/tests/unit/test_documents.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, replace from datetime import datetime from decimal import Decimal -from typing import Any, cast +from typing import Any, Self, cast import pytest @@ -654,7 +654,7 @@ def serialize_members(self, serializer: ShapeSerializer) -> None: ms.entry(key, lambda vs: vs.write_string(target_schema, value)) # type: ignore @classmethod - def deserialize(cls, deserializer: ShapeDeserializer) -> "DocumentSerdeShape": + def deserialize(cls, deserializer: ShapeDeserializer) -> Self: kwargs: dict[str, Any] = {} def _consumer(schema: Schema, de: ShapeDeserializer) -> None: