diff --git a/generators/python/sdk/versions.yml b/generators/python/sdk/versions.yml index b57e63daf1d..44d097ef8f3 100644 --- a/generators/python/sdk/versions.yml +++ b/generators/python/sdk/versions.yml @@ -1,4 +1,13 @@ # For unreleased changes, use unreleased.yml +- version: 4.0.0-rc3 + irVersion: 53 + changelogEntry: + - type: fix + summary: | + Pydantic models now call update forward refs on non-uion circular references. This + prevents runtime errors in certain cases where types self reference itself through + a union. + - version: 4.0.0-rc2 irVersion: 53 changelogEntry: diff --git a/generators/python/src/fern_python/generators/context/pydantic_generator_context.py b/generators/python/src/fern_python/generators/context/pydantic_generator_context.py index a0f5aeb94b6..187ff67daf8 100644 --- a/generators/python/src/fern_python/generators/context/pydantic_generator_context.py +++ b/generators/python/src/fern_python/generators/context/pydantic_generator_context.py @@ -68,6 +68,10 @@ def get_class_reference_for_type_id( def does_circularly_reference_itself(self, type_id: ir_types.TypeId) -> bool: ... + @abstractmethod + def get_non_union_circular_references(self) -> Set[ir_types.TypeId]: + ... + @abstractmethod def do_types_reference_each_other(self, a: ir_types.TypeId, b: ir_types.TypeId) -> bool: ... diff --git a/generators/python/src/fern_python/generators/context/pydantic_generator_context_impl.py b/generators/python/src/fern_python/generators/context/pydantic_generator_context_impl.py index ded9e811846..00d3997b607 100644 --- a/generators/python/src/fern_python/generators/context/pydantic_generator_context_impl.py +++ b/generators/python/src/fern_python/generators/context/pydantic_generator_context_impl.py @@ -44,6 +44,15 @@ def __init__( self._allow_leveraging_defaults = allow_leveraging_defaults self._reserved_names: Set[str] = reserved_names or set() + self._non_union_self_referencing_type_ids = set() + for id, type in self.ir.types.items(): + if ( + id in type.referenced_types + and type.shape.get_as_union().type != "union" + and type.shape.get_as_union().type != "undiscriminatedUnion" + ): + self._non_union_self_referencing_type_ids.add(id) + def get_module_path_in_project(self, module_path: AST.ModulePath) -> AST.ModulePath: return self._project_module_path + module_path @@ -135,6 +144,9 @@ def get_class_reference_for_type_id( def does_circularly_reference_itself(self, type_id: ir_types.TypeId) -> bool: return self.does_type_reference_other_type(type_id, type_id) + def get_non_union_circular_references(self) -> Set[ir_types.TypeId]: + return self._non_union_self_referencing_type_ids + def do_types_reference_each_other(self, a: ir_types.TypeId, b: ir_types.TypeId) -> bool: return self.does_type_reference_other_type(a, b) and self.does_type_reference_other_type(b, a) diff --git a/generators/python/src/fern_python/generators/pydantic_model/fern_aware_pydantic_model.py b/generators/python/src/fern_python/generators/pydantic_model/fern_aware_pydantic_model.py index 44177ce7466..b752490e569 100644 --- a/generators/python/src/fern_python/generators/pydantic_model/fern_aware_pydantic_model.py +++ b/generators/python/src/fern_python/generators/pydantic_model/fern_aware_pydantic_model.py @@ -186,15 +186,20 @@ def _type_id_for_forward_ref(self) -> Optional[ir_types.TypeId]: def _must_import_after_current_declaration(self, type_name: ir_types.DeclaredTypeName) -> bool: type_id_to_reference = self._type_id_for_forward_ref() - is_circular_reference = False + should_import_after = False if type_id_to_reference is not None: - is_circular_reference = self._context.does_type_reference_other_type( + should_import_after = self._context.does_type_reference_other_type( type_id=type_name.type_id, other_type_id=type_id_to_reference ) - if is_circular_reference: + is_referencing_circular_reference = type_name.type_id in self._context.get_non_union_circular_references() + if is_referencing_circular_reference: + should_import_after = is_referencing_circular_reference + + if should_import_after: self._model_contains_forward_refs = True - return is_circular_reference + + return should_import_after def add_method( self, diff --git a/seed/python-sdk/grpc-proto/.mock/proto/google/api/field_behavior.proto b/seed/python-sdk/grpc-proto/.mock/proto/google/api/field_behavior.proto deleted file mode 100644 index 128799c558d..00000000000 --- a/seed/python-sdk/grpc-proto/.mock/proto/google/api/field_behavior.proto +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -import "google/protobuf/descriptor.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "FieldBehaviorProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -extend google.protobuf.FieldOptions { - // A designation of a specific field behavior (required, output only, etc.) - // in protobuf messages. - // - // Examples: - // - // string name = 1 [(google.api.field_behavior) = REQUIRED]; - // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; - // google.protobuf.Duration ttl = 1 - // [(google.api.field_behavior) = INPUT_ONLY]; - // google.protobuf.Timestamp expire_time = 1 - // [(google.api.field_behavior) = OUTPUT_ONLY, - // (google.api.field_behavior) = IMMUTABLE]; - repeated google.api.FieldBehavior field_behavior = 1052; -} - -// An indicator of the behavior of a given field (for example, that a field -// is required in requests, or given as output but ignored as input). -// This **does not** change the behavior in protocol buffers itself; it only -// denotes the behavior and may affect how API tooling handles the field. -// -// Note: This enum **may** receive new values in the future. -enum FieldBehavior { - // Conventional default for enums. Do not use this. - FIELD_BEHAVIOR_UNSPECIFIED = 0; - - // Specifically denotes a field as optional. - // While all fields in protocol buffers are optional, this may be specified - // for emphasis if appropriate. - OPTIONAL = 1; - - // Denotes a field as required. - // This indicates that the field **must** be provided as part of the request, - // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). - REQUIRED = 2; - - // Denotes a field as output only. - // This indicates that the field is provided in responses, but including the - // field in a request does nothing (the server *must* ignore it and - // *must not* throw an error as a result of the field's presence). - OUTPUT_ONLY = 3; - - // Denotes a field as input only. - // This indicates that the field is provided in requests, and the - // corresponding field is not included in output. - INPUT_ONLY = 4; - - // Denotes a field as immutable. - // This indicates that the field may be set once in a request to create a - // resource, but may not be changed thereafter. - IMMUTABLE = 5; - - // Denotes that a (repeated) field is an unordered list. - // This indicates that the service may provide the elements of the list - // in any arbitrary order, rather than the order the user originally - // provided. Additionally, the list's order may or may not be stable. - UNORDERED_LIST = 6; - - // Denotes that this field returns a non-empty default value if not set. - // This indicates that if the user provides the empty value in a request, - // a non-empty value will be returned. The user will not be aware of what - // non-empty value to expect. - NON_EMPTY_DEFAULT = 7; - - // Denotes that the field in a resource (a message annotated with - // google.api.resource) is used in the resource name to uniquely identify the - // resource. For AIP-compliant APIs, this should only be applied to the - // `name` field on the resource. - // - // This behavior should not be applied to references to other resources within - // the message. - // - // The identifier field of resources often have different field behavior - // depending on the request it is embedded in (e.g. for Create methods name - // is optional and unused, while for Update methods it is required). Instead - // of method-specific annotations, only `IDENTIFIER` is required. - IDENTIFIER = 8; -} \ No newline at end of file