diff --git a/.chronus/changes/python-backcompatTspPadding-2025-10-18-17-23-22.md b/.chronus/changes/python-backcompatTspPadding-2025-10-18-17-23-22.md new file mode 100644 index 00000000000..3d266c69f9f --- /dev/null +++ b/.chronus/changes/python-backcompatTspPadding-2025-10-18-17-23-22.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Keep original client name for backcompat reasons when the name is only padded for tsp generations \ No newline at end of file diff --git a/packages/http-client-python/generator/pygen/codegen/models/code_model.py b/packages/http-client-python/generator/pygen/codegen/models/code_model.py index 6d2796d49e2..e4adbffea78 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/code_model.py +++ b/packages/http-client-python/generator/pygen/codegen/models/code_model.py @@ -488,3 +488,11 @@ def _get_relative_generation_dir(self, root_dir: Path, namespace: str) -> Path: @property def has_operation_named_list(self) -> bool: return any(o.name.lower() == "list" for c in self.clients for og in c.operation_groups for o in og.operations) + + @property + def has_padded_model_property(self) -> bool: + for model_type in self.model_types: + for prop in model_type.properties: + if prop.original_tsp_name: + return True + return False diff --git a/packages/http-client-python/generator/pygen/codegen/models/property.py b/packages/http-client-python/generator/pygen/codegen/models/property.py index 2d7c12d304c..dbdf01aa7d0 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/property.py +++ b/packages/http-client-python/generator/pygen/codegen/models/property.py @@ -39,6 +39,7 @@ def __init__( self.flattened_names: list[str] = yaml_data.get("flattenedNames", []) self.is_multipart_file_input: bool = yaml_data.get("isMultipartFileInput", False) self.flatten = self.yaml_data.get("flatten", False) and not getattr(self.type, "flattened_property", False) + self.original_tsp_name: Optional[str] = self.yaml_data.get("originalTspName") def pylint_disable(self) -> str: retval: str = "" diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py index 5c1719ccdaf..544c984ba91 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/model_serializer.py @@ -335,6 +335,9 @@ def declare_property(self, prop: Property) -> str: if prop.xml_metadata: args.append(f"xml={prop.xml_metadata}") + if prop.original_tsp_name: + args.append(f'original_tsp_name="{prop.original_tsp_name}"') + field = "rest_discriminator" if prop.is_discriminator else "rest_field" type_ignore = ( " # type: ignore" diff --git a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 index 3158f8c46e6..a62c206c27d 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/model_base.py.jinja2 @@ -636,6 +636,12 @@ class Model(_MyMutableMapping): if not rf._rest_name_input: rf._rest_name_input = attr cls._attr_to_rest_field: dict[str, _RestField] = dict(attr_to_rest_field.items()) + {% if code_model.has_padded_model_property %} + cls._backcompat_attr_to_rest_field: dict[str, _RestField] = { + Model._get_backcompat_attribute_name(cls._attr_to_rest_field, attr): rf for attr, rf in cls + ._attr_to_rest_field.items() + } + {% endif %} cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}") return super().__new__(cls) @@ -645,6 +651,18 @@ class Model(_MyMutableMapping): if hasattr(base, "__mapping__"): base.__mapping__[discriminator or cls.__name__] = cls # type: ignore + {% if code_model.has_padded_model_property %} + @classmethod + def _get_backcompat_attribute_name(cls, attr_to_rest_field: dict[str, "_RestField"], attr_name: str) -> str: + rest_field_obj = attr_to_rest_field.get(attr_name) # pylint: disable=protected-access + if rest_field_obj is None: + return attr_name + original_tsp_name = getattr(rest_field_obj, "_original_tsp_name", None) # pylint: disable=protected-access + if original_tsp_name: + return original_tsp_name + return attr_name + {% endif %} + @classmethod def _get_discriminator(cls, exist_discriminators) -> typing.Optional["_RestField"]: for v in cls.__dict__.values(): @@ -971,6 +989,9 @@ def _failsafe_deserialize_xml( return None +{% if code_model.has_padded_model_property %} +# pylint: disable=too-many-instance-attributes +{% endif %} class _RestField: def __init__( self, @@ -983,6 +1004,9 @@ class _RestField: format: typing.Optional[str] = None, is_multipart_file_input: bool = False, xml: typing.Optional[dict[str, typing.Any]] = None, + {% if code_model.has_padded_model_property %} + original_tsp_name: typing.Optional[str] = None, + {% endif %} ): self._type = type self._rest_name_input = name @@ -994,6 +1018,9 @@ class _RestField: self._format = format self._is_multipart_file_input = is_multipart_file_input self._xml = xml if xml is not None else {} + {% if code_model.has_padded_model_property %} + self._original_tsp_name = original_tsp_name + {% endif %} @property def _class_type(self) -> typing.Any: @@ -1045,6 +1072,9 @@ def rest_field( format: typing.Optional[str] = None, is_multipart_file_input: bool = False, xml: typing.Optional[dict[str, typing.Any]] = None, + {% if code_model.has_padded_model_property %} + original_tsp_name: typing.Optional[str] = None, + {% endif %} ) -> typing.Any: return _RestField( name=name, @@ -1054,6 +1084,9 @@ def rest_field( format=format, is_multipart_file_input=is_multipart_file_input, xml=xml, + {% if code_model.has_padded_model_property %} + original_tsp_name=original_tsp_name, + {% endif %} ) diff --git a/packages/http-client-python/generator/pygen/preprocess/__init__.py b/packages/http-client-python/generator/pygen/preprocess/__init__.py index c62b717f491..315d8ff9078 100644 --- a/packages/http-client-python/generator/pygen/preprocess/__init__.py +++ b/packages/http-client-python/generator/pygen/preprocess/__init__.py @@ -236,7 +236,7 @@ def add_body_param_type( body_parameter["type"]["types"].insert(1, any_obj_list_or_dict) code_model["types"].append(body_parameter["type"]) - def pad_reserved_words(self, name: str, pad_type: PadType): + def pad_reserved_words(self, name: str, pad_type: PadType, yaml_type: dict[str, Any]) -> str: # we want to pad hidden variables as well if not name: # we'll pass in empty operation groups sometime etc. @@ -250,6 +250,10 @@ def pad_reserved_words(self, name: str, pad_type: PadType): name_prefix = "_" if name[0] == "_" else "" name = name[1:] if name[0] == "_" else name if name.lower() in reserved_words[pad_type]: + if self.is_tsp and name.lower() in TSP_RESERVED_WORDS.get(pad_type, []): + # to maintain backcompat for cases where we pad in tsp but not in autorest, + # if we have a tsp reserved word, we also want to keep track of the original name for backcompat + yaml_type["originalTspName"] = name_prefix + name return name_prefix + name + pad_type return name_prefix + name @@ -257,11 +261,13 @@ def update_types(self, yaml_data: list[dict[str, Any]]) -> None: for type in yaml_data: for property in type.get("properties", []): property["description"] = update_description(property.get("description", "")) - property["clientName"] = self.pad_reserved_words(property["clientName"].lower(), PadType.PROPERTY) + property["clientName"] = self.pad_reserved_words( + property["clientName"].lower(), PadType.PROPERTY, property + ) add_redefined_builtin_info(property["clientName"], property) if type.get("name"): pad_type = PadType.MODEL if type["type"] == "model" else PadType.ENUM_CLASS - name = self.pad_reserved_words(type["name"], pad_type) + name = self.pad_reserved_words(type["name"], pad_type, type) type["name"] = name[0].upper() + name[1:] type["description"] = update_description(type.get("description", ""), type["name"]) type["snakeCaseName"] = to_snake_case(type["name"]) @@ -269,7 +275,7 @@ def update_types(self, yaml_data: list[dict[str, Any]]) -> None: # we're enums values_to_add = [] for value in type["values"]: - padded_name = self.pad_reserved_words(value["name"].lower(), PadType.ENUM_VALUE).upper() + padded_name = self.pad_reserved_words(value["name"].lower(), PadType.ENUM_VALUE, value).upper() if self.version_tolerant: if padded_name[0] in "0123456789": padded_name = "ENUM_" + padded_name @@ -364,12 +370,14 @@ def get_operation_updater(self, yaml_data: dict[str, Any]) -> Callable[[dict[str def update_parameter(self, yaml_data: dict[str, Any]) -> None: yaml_data["description"] = update_description(yaml_data.get("description", "")) if not (yaml_data["location"] == "header" and yaml_data["clientName"] in ("content_type", "accept")): - yaml_data["clientName"] = self.pad_reserved_words(yaml_data["clientName"].lower(), PadType.PARAMETER) + yaml_data["clientName"] = self.pad_reserved_words( + yaml_data["clientName"].lower(), PadType.PARAMETER, yaml_data + ) if yaml_data.get("propertyToParameterName"): # need to create a new one with padded keys and values yaml_data["propertyToParameterName"] = { - self.pad_reserved_words(prop, PadType.PROPERTY): self.pad_reserved_words( - param_name, PadType.PARAMETER + self.pad_reserved_words(prop, PadType.PROPERTY, yaml_data): self.pad_reserved_words( + param_name, PadType.PARAMETER, yaml_data ).lower() for prop, param_name in yaml_data["propertyToParameterName"].items() } @@ -390,15 +398,17 @@ def update_operation( *, is_overload: bool = False, ) -> None: - yaml_data["groupName"] = self.pad_reserved_words(yaml_data["groupName"], PadType.OPERATION_GROUP) + yaml_data["groupName"] = self.pad_reserved_words(yaml_data["groupName"], PadType.OPERATION_GROUP, yaml_data) yaml_data["groupName"] = to_snake_case(yaml_data["groupName"]) yaml_data["name"] = yaml_data["name"].lower() if yaml_data.get("isLroInitialOperation") is True: yaml_data["name"] = ( - "_" + self.pad_reserved_words(extract_original_name(yaml_data["name"]), PadType.METHOD) + "_initial" + "_" + + self.pad_reserved_words(extract_original_name(yaml_data["name"]), PadType.METHOD, yaml_data) + + "_initial" ) else: - yaml_data["name"] = self.pad_reserved_words(yaml_data["name"], PadType.METHOD) + yaml_data["name"] = self.pad_reserved_words(yaml_data["name"], PadType.METHOD, yaml_data) yaml_data["description"] = update_description(yaml_data["description"], yaml_data["name"]) yaml_data["summary"] = update_description(yaml_data.get("summary", "")) body_parameter = yaml_data.get("bodyParameter") @@ -485,7 +495,7 @@ def update_paging_operation( item_type = item_type or yaml_data["itemType"]["elementType"] if yaml_data.get("nextOperation"): yaml_data["nextOperation"]["groupName"] = self.pad_reserved_words( - yaml_data["nextOperation"]["groupName"], PadType.OPERATION_GROUP + yaml_data["nextOperation"]["groupName"], PadType.OPERATION_GROUP, yaml_data["nextOperation"] ) yaml_data["nextOperation"]["groupName"] = to_snake_case(yaml_data["nextOperation"]["groupName"]) for response in yaml_data["nextOperation"].get("responses", []): @@ -503,10 +513,11 @@ def update_operation_groups(self, code_model: dict[str, Any], client: dict[str, operation_group["identifyName"] = self.pad_reserved_words( operation_group.get("name", operation_group["propertyName"]), PadType.OPERATION_GROUP, + operation_group, ) operation_group["identifyName"] = to_snake_case(operation_group["identifyName"]) operation_group["propertyName"] = self.pad_reserved_words( - operation_group["propertyName"], PadType.OPERATION_GROUP + operation_group["propertyName"], PadType.OPERATION_GROUP, operation_group ) operation_group["propertyName"] = to_snake_case(operation_group["propertyName"]) operation_group["className"] = update_operation_group_class_name( diff --git a/packages/http-client-python/generator/test/unittests/test_name_converter.py b/packages/http-client-python/generator/test/unittests/test_name_converter.py index 7e358f66103..c205c0b0bae 100644 --- a/packages/http-client-python/generator/test/unittests/test_name_converter.py +++ b/packages/http-client-python/generator/test/unittests/test_name_converter.py @@ -8,7 +8,7 @@ def pad_reserved_words(name: str, pad_type: PadType) -> str: - return PreProcessPlugin(output_folder="").pad_reserved_words(name, pad_type) + return PreProcessPlugin(output_folder="").pad_reserved_words(name, pad_type, {}) def test_escaped_reserved_words():