From 9864c769c7bbff3dacd9eb2c984316068f3b142a Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 24 Jul 2026 11:34:40 -0700 Subject: [PATCH 1/8] fix(http-client-python): api_version validation decorator reads correct config attribute name The generated _validation.py @api_version_validation decorator hardcoded `client._config.api_version`, but the config attribute name is derived from the API-version parameter's client_name. Specs that name the versioning parameter something other than `apiVersion` (e.g. Azure Storage Blob's `@apiVersion @header("x-ms-version") version: string`, which produces `self.version`) hit an AttributeError that the decorator silently swallowed, disabling all API-version validation for those clients. Thread the real attribute name into the decorator via a `client_api_version_name` kwarg, emitted only when it differs from the default `api_version`. Also fix a latent NameError in the decorator's `unsupported` error-message branch that referenced an out-of-scope `version` variable. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e983f91-eed5-4191-b531-0eca9e696313 --- ...rsion-validation-attr-2026-7-24-11-30-0.md | 7 + .../codegen/serializers/builder_serializer.py | 6 + .../codegen/templates/validation.py.jinja2 | 7 +- .../tests/unit/test_api_version_validation.py | 169 ++++++++++++++++++ 4 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 .chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md create mode 100644 packages/http-client-python/tests/unit/test_api_version_validation.py diff --git a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md new file mode 100644 index 00000000000..78ee304988b --- /dev/null +++ b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. Previously it hardcoded `client._config.api_version`, but the config attribute name comes from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. Azure Storage's `@apiVersion @header("x-ms-version") version: string`, which produces `self.version`), the hardcoded lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now threads the real attribute name into the decorator via a `client_api_version_name` kwarg (emitted only when it differs from the default `api_version`). Also fixes a latent `NameError` in the decorator's `unsupported` error-message branch that referenced an out-of-scope `version` variable. diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index b1da889bed6..9020b98f40a 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -632,6 +632,12 @@ def _api_version_validation(self, builder: OperationType) -> str: if retval: if builder.api_versions: retval.append(f" api_versions_list={builder.api_versions},") + api_version_param = next( + (p for p in builder.client.config.parameters.parameters if p.is_api_version), + None, + ) + if api_version_param and api_version_param.client_name != "api_version": + retval.append(f' client_api_version_name="{api_version_param.client_name}",') retval_str = "\n".join(retval) return f"@api_version_validation(\n{retval_str}\n)" return "" diff --git a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 index fe534e7fe44..bfbab800fda 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 @@ -7,6 +7,7 @@ def api_version_validation(**kwargs): params_added_on = kwargs.pop("params_added_on", {}) method_added_on = kwargs.pop("method_added_on", "") api_versions_list = kwargs.pop("api_versions_list", []) + client_api_version_name = kwargs.pop("client_api_version_name", "api_version") def _index_with_default(value: str, default: int = -1) -> int: """Get the index of value in lst, or return default if not found. @@ -29,7 +30,7 @@ def api_version_validation(**kwargs): try: # this assumes the client has an _api_version attribute client = args[0] - client_api_version = client._config.api_version # pylint: disable=protected-access + client_api_version = getattr(client._config, client_api_version_name) # pylint: disable=protected-access except AttributeError: return func(*args, **kwargs) @@ -48,8 +49,8 @@ def api_version_validation(**kwargs): if unsupported: raise ValueError("".join([ f"'{param}' is not available in API version {client_api_version}. " - f"Use service API version {version} or newer.\n" - for param, version in unsupported.items() + f"Use service API version {param_api_version} or newer.\n" + for param, param_api_version in unsupported.items() ])) return func(*args, **kwargs) return wrapper diff --git a/packages/http-client-python/tests/unit/test_api_version_validation.py b/packages/http-client-python/tests/unit/test_api_version_validation.py new file mode 100644 index 00000000000..edf4addbc12 --- /dev/null +++ b/packages/http-client-python/tests/unit/test_api_version_validation.py @@ -0,0 +1,169 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +"""Tests for the ``@api_version_validation`` decorator emission. + +Regression coverage for the bug where the generated ``_validation.py`` decorator +hardcoded ``client._config.api_version``. When the versioning parameter is named +something other than ``apiVersion`` (e.g. Azure Storage names it ``version`` via +``@apiVersion @header("x-ms-version") version: string``), the config attribute is +``self.version`` and NOT ``self.api_version``. The hardcoded lookup then raised +``AttributeError`` which the decorator silently swallowed, disabling ALL +api-version validation for those clients. + +The fix threads the real config attribute name (the api-version parameter's +``client_name``) into the decorator via a ``client_api_version_name`` kwarg, +emitted only when it differs from the default ``api_version``. +""" + +import pytest + +from pygen.codegen.models import ( + Client, + CodeModel, + Operation, + ParameterList, + RequestBuilder, +) +from pygen.codegen.models.parameter import ConfigParameter +from pygen.codegen.models.parameter_list import ( + ClientGlobalParameterList, + RequestBuilderParameterList, +) +from pygen.codegen.models.primitive_types import StringType +from pygen.codegen.serializers.builder_serializer import OperationSerializer + + +@pytest.fixture +def code_model(): + return CodeModel( + { + "clients": [ + { + "name": "client", + "namespace": "blah", + "moduleName": "blah", + "parameters": [], + "url": "", + "operationGroups": [], + } + ], + "namespace": "namespace", + }, + options={ + "show-send-request": True, + "builders-visibility": "public", + "show-operations": True, + "models-mode": "dpg", + "version-tolerant": True, + }, + ) + + +def _client(code_model): + return Client( + { + "name": "client", + "namespace": "blah", + "moduleName": "blah", + "parameters": [], + "url": "", + "operationGroups": [], + }, + code_model, + parameters=ClientGlobalParameterList({}, code_model, parameters=[]), + ) + + +def _api_version_config_parameter(code_model, client_name: str) -> ConfigParameter: + return ConfigParameter( + yaml_data={ + "clientName": client_name, + "optional": True, + "location": "query", + "wireName": "api-version", + "implementation": "Client", + "isApiVersion": True, + "description": "The API version.", + }, + code_model=code_model, + type=StringType({"type": "string"}, code_model), + ) + + +def _operation(code_model, client): + request_builder = RequestBuilder( + yaml_data={ + "url": "http://fake.com", + "method": "GET", + "groupName": "blah", + "isOverload": False, + "apiVersions": ["2023-01-01"], + }, + client=client, + code_model=code_model, + name="do_thing_request", + parameters=RequestBuilderParameterList({}, code_model, parameters=[]), + ) + return Operation( + yaml_data={ + "url": "http://fake.com", + "method": "GET", + "groupName": "blah", + "isOverload": False, + "apiVersions": ["2023-01-01"], + "addedOn": "2023-01-01", + }, + client=client, + code_model=code_model, + request_builder=request_builder, + name="do_thing", + parameters=ParameterList({}, code_model, parameters=[]), + responses=[], + exceptions=[], + ) + + +def _serializer(code_model): + return OperationSerializer(code_model, async_mode=False, client_namespace="blah") + + +def test_emits_client_api_version_name_when_param_is_not_api_version(code_model): + # Storage-like: the versioning parameter is named ``version`` -> config attr + # is ``self.version``, so the decorator must be told to read that attribute. + client = _client(code_model) + client.config.parameters.parameters.append( + _api_version_config_parameter(code_model, "version") + ) + operation = _operation(code_model, client) + + decorator = _serializer(code_model)._api_version_validation(operation) + + assert ' client_api_version_name="version",' in decorator + + +def test_no_kwarg_when_param_is_conventional_api_version(code_model): + # Conventional ``apiVersion`` -> config attr is ``self.api_version`` which is + # the decorator default, so no kwarg should be emitted (keeps regen churn low). + client = _client(code_model) + client.config.parameters.parameters.append( + _api_version_config_parameter(code_model, "api_version") + ) + operation = _operation(code_model, client) + + decorator = _serializer(code_model)._api_version_validation(operation) + + assert decorator # decorator is still emitted (operation has addedOn) + assert "client_api_version_name" not in decorator + + +def test_no_kwarg_when_no_api_version_param(code_model): + client = _client(code_model) + operation = _operation(code_model, client) + + decorator = _serializer(code_model)._api_version_validation(operation) + + assert decorator + assert "client_api_version_name" not in decorator From d218e7372f3e90ad4d98909370628adaeb40428c Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 24 Jul 2026 11:52:27 -0700 Subject: [PATCH 2/8] Drop incidental version f-string NameError change; keep only api_version attribute fix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e983f91-eed5-4191-b531-0eca9e696313 --- .../fix-api-version-validation-attr-2026-7-24-11-30-0.md | 2 +- .../generator/pygen/codegen/templates/validation.py.jinja2 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md index 78ee304988b..33d98b20a79 100644 --- a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md +++ b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md @@ -4,4 +4,4 @@ packages: - "@typespec/http-client-python" --- -Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. Previously it hardcoded `client._config.api_version`, but the config attribute name comes from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. Azure Storage's `@apiVersion @header("x-ms-version") version: string`, which produces `self.version`), the hardcoded lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now threads the real attribute name into the decorator via a `client_api_version_name` kwarg (emitted only when it differs from the default `api_version`). Also fixes a latent `NameError` in the decorator's `unsupported` error-message branch that referenced an out-of-scope `version` variable. +Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. Previously it hardcoded `client._config.api_version`, but the config attribute name comes from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. Azure Storage's `@apiVersion @header("x-ms-version") version: string`, which produces `self.version`), the hardcoded lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now threads the real attribute name into the decorator via a `client_api_version_name` kwarg (emitted only when it differs from the default `api_version`). diff --git a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 index bfbab800fda..16b1234c10d 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 @@ -49,8 +49,8 @@ def api_version_validation(**kwargs): if unsupported: raise ValueError("".join([ f"'{param}' is not available in API version {client_api_version}. " - f"Use service API version {param_api_version} or newer.\n" - for param, param_api_version in unsupported.items() + f"Use service API version {version} or newer.\n" + for param, version in unsupported.items() ])) return func(*args, **kwargs) return wrapper From 7fde618e51866ff67809b9a6a27a1abf756297d2 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 24 Jul 2026 12:49:11 -0700 Subject: [PATCH 3/8] Tighten changelog wording Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e983f91-eed5-4191-b531-0eca9e696313 --- .../fix-api-version-validation-attr-2026-7-24-11-30-0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md index 33d98b20a79..6ee21c24afe 100644 --- a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md +++ b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md @@ -4,4 +4,4 @@ packages: - "@typespec/http-client-python" --- -Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. Previously it hardcoded `client._config.api_version`, but the config attribute name comes from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. Azure Storage's `@apiVersion @header("x-ms-version") version: string`, which produces `self.version`), the hardcoded lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now threads the real attribute name into the decorator via a `client_api_version_name` kwarg (emitted only when it differs from the default `api_version`). +Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. It previously hardcoded `client._config.api_version`, but the attribute name is derived from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. `self.version`), the lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now passes the real attribute name to the decorator via a `client_api_version_name` kwarg. From 0ad42842e280da1c92252c15df9e96c578e3af85 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 24 Jul 2026 13:36:45 -0700 Subject: [PATCH 4/8] Keep protected _config access on its own line so black can't detach the pylint suppression Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e983f91-eed5-4191-b531-0eca9e696313 --- .../generator/pygen/codegen/templates/validation.py.jinja2 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 index 16b1234c10d..e02867acf0b 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 @@ -30,7 +30,8 @@ def api_version_validation(**kwargs): try: # this assumes the client has an _api_version attribute client = args[0] - client_api_version = getattr(client._config, client_api_version_name) # pylint: disable=protected-access + config = client._config # pylint: disable=protected-access + client_api_version = getattr(config, client_api_version_name) except AttributeError: return func(*args, **kwargs) From 34a487f971fbc6a76208679a7eb3c2f39c4ca8bb Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 24 Jul 2026 13:47:59 -0700 Subject: [PATCH 5/8] Read api_version via a direct config attribute accessor instead of getattr Thread a client_api_version_getter (lambda config: config.) into the decorator so the generated code reads self._config. directly rather than via a getattr string lookup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e983f91-eed5-4191-b531-0eca9e696313 --- ...-version-validation-attr-2026-7-24-11-30-0.md | 2 +- .../codegen/serializers/builder_serializer.py | 4 +++- .../pygen/codegen/templates/validation.py.jinja2 | 4 ++-- .../tests/unit/test_api_version_validation.py | 16 ++++++++-------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md index 6ee21c24afe..925e8ea7d93 100644 --- a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md +++ b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md @@ -4,4 +4,4 @@ packages: - "@typespec/http-client-python" --- -Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. It previously hardcoded `client._config.api_version`, but the attribute name is derived from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. `self.version`), the lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now passes the real attribute name to the decorator via a `client_api_version_name` kwarg. +Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. It previously hardcoded `client._config.api_version`, but the attribute name is derived from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. `self.version`), the lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now passes an accessor for the real attribute to the decorator via a `client_api_version_getter` kwarg. diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 9020b98f40a..7c78bd93852 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -637,7 +637,9 @@ def _api_version_validation(self, builder: OperationType) -> str: None, ) if api_version_param and api_version_param.client_name != "api_version": - retval.append(f' client_api_version_name="{api_version_param.client_name}",') + retval.append( + f" client_api_version_getter=lambda config: config.{api_version_param.client_name}," + ) retval_str = "\n".join(retval) return f"@api_version_validation(\n{retval_str}\n)" return "" diff --git a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 index e02867acf0b..6cc00b2f876 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 @@ -7,7 +7,7 @@ def api_version_validation(**kwargs): params_added_on = kwargs.pop("params_added_on", {}) method_added_on = kwargs.pop("method_added_on", "") api_versions_list = kwargs.pop("api_versions_list", []) - client_api_version_name = kwargs.pop("client_api_version_name", "api_version") + client_api_version_getter = kwargs.pop("client_api_version_getter", lambda config: config.api_version) def _index_with_default(value: str, default: int = -1) -> int: """Get the index of value in lst, or return default if not found. @@ -31,7 +31,7 @@ def api_version_validation(**kwargs): # this assumes the client has an _api_version attribute client = args[0] config = client._config # pylint: disable=protected-access - client_api_version = getattr(config, client_api_version_name) + client_api_version = client_api_version_getter(config) except AttributeError: return func(*args, **kwargs) diff --git a/packages/http-client-python/tests/unit/test_api_version_validation.py b/packages/http-client-python/tests/unit/test_api_version_validation.py index edf4addbc12..72979b149e4 100644 --- a/packages/http-client-python/tests/unit/test_api_version_validation.py +++ b/packages/http-client-python/tests/unit/test_api_version_validation.py @@ -13,9 +13,9 @@ ``AttributeError`` which the decorator silently swallowed, disabling ALL api-version validation for those clients. -The fix threads the real config attribute name (the api-version parameter's -``client_name``) into the decorator via a ``client_api_version_name`` kwarg, -emitted only when it differs from the default ``api_version``. +The fix threads an accessor for the real config attribute into the decorator via +a ``client_api_version_getter`` kwarg (``lambda config: config.``), +emitted only when the attribute name differs from the default ``api_version``. """ import pytest @@ -130,9 +130,9 @@ def _serializer(code_model): return OperationSerializer(code_model, async_mode=False, client_namespace="blah") -def test_emits_client_api_version_name_when_param_is_not_api_version(code_model): +def test_emits_client_api_version_getter_when_param_is_not_api_version(code_model): # Storage-like: the versioning parameter is named ``version`` -> config attr - # is ``self.version``, so the decorator must be told to read that attribute. + # is ``self.version``, so the decorator must read that attribute directly. client = _client(code_model) client.config.parameters.parameters.append( _api_version_config_parameter(code_model, "version") @@ -141,7 +141,7 @@ def test_emits_client_api_version_name_when_param_is_not_api_version(code_model) decorator = _serializer(code_model)._api_version_validation(operation) - assert ' client_api_version_name="version",' in decorator + assert " client_api_version_getter=lambda config: config.version," in decorator def test_no_kwarg_when_param_is_conventional_api_version(code_model): @@ -156,7 +156,7 @@ def test_no_kwarg_when_param_is_conventional_api_version(code_model): decorator = _serializer(code_model)._api_version_validation(operation) assert decorator # decorator is still emitted (operation has addedOn) - assert "client_api_version_name" not in decorator + assert "client_api_version_getter" not in decorator def test_no_kwarg_when_no_api_version_param(code_model): @@ -166,4 +166,4 @@ def test_no_kwarg_when_no_api_version_param(code_model): decorator = _serializer(code_model)._api_version_validation(operation) assert decorator - assert "client_api_version_name" not in decorator + assert "client_api_version_getter" not in decorator From 8236f8ca5eca3c20ae13d24e17af30cef0db6229 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 24 Jul 2026 13:51:24 -0700 Subject: [PATCH 6/8] Revert to getattr-based config attribute lookup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e983f91-eed5-4191-b531-0eca9e696313 --- ...-version-validation-attr-2026-7-24-11-30-0.md | 2 +- .../codegen/serializers/builder_serializer.py | 4 +--- .../pygen/codegen/templates/validation.py.jinja2 | 4 ++-- .../tests/unit/test_api_version_validation.py | 16 ++++++++-------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md index 925e8ea7d93..6ee21c24afe 100644 --- a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md +++ b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md @@ -4,4 +4,4 @@ packages: - "@typespec/http-client-python" --- -Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. It previously hardcoded `client._config.api_version`, but the attribute name is derived from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. `self.version`), the lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now passes an accessor for the real attribute to the decorator via a `client_api_version_getter` kwarg. +Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. It previously hardcoded `client._config.api_version`, but the attribute name is derived from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. `self.version`), the lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now passes the real attribute name to the decorator via a `client_api_version_name` kwarg. diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 7c78bd93852..9020b98f40a 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -637,9 +637,7 @@ def _api_version_validation(self, builder: OperationType) -> str: None, ) if api_version_param and api_version_param.client_name != "api_version": - retval.append( - f" client_api_version_getter=lambda config: config.{api_version_param.client_name}," - ) + retval.append(f' client_api_version_name="{api_version_param.client_name}",') retval_str = "\n".join(retval) return f"@api_version_validation(\n{retval_str}\n)" return "" diff --git a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 index 6cc00b2f876..e02867acf0b 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 @@ -7,7 +7,7 @@ def api_version_validation(**kwargs): params_added_on = kwargs.pop("params_added_on", {}) method_added_on = kwargs.pop("method_added_on", "") api_versions_list = kwargs.pop("api_versions_list", []) - client_api_version_getter = kwargs.pop("client_api_version_getter", lambda config: config.api_version) + client_api_version_name = kwargs.pop("client_api_version_name", "api_version") def _index_with_default(value: str, default: int = -1) -> int: """Get the index of value in lst, or return default if not found. @@ -31,7 +31,7 @@ def api_version_validation(**kwargs): # this assumes the client has an _api_version attribute client = args[0] config = client._config # pylint: disable=protected-access - client_api_version = client_api_version_getter(config) + client_api_version = getattr(config, client_api_version_name) except AttributeError: return func(*args, **kwargs) diff --git a/packages/http-client-python/tests/unit/test_api_version_validation.py b/packages/http-client-python/tests/unit/test_api_version_validation.py index 72979b149e4..edf4addbc12 100644 --- a/packages/http-client-python/tests/unit/test_api_version_validation.py +++ b/packages/http-client-python/tests/unit/test_api_version_validation.py @@ -13,9 +13,9 @@ ``AttributeError`` which the decorator silently swallowed, disabling ALL api-version validation for those clients. -The fix threads an accessor for the real config attribute into the decorator via -a ``client_api_version_getter`` kwarg (``lambda config: config.``), -emitted only when the attribute name differs from the default ``api_version``. +The fix threads the real config attribute name (the api-version parameter's +``client_name``) into the decorator via a ``client_api_version_name`` kwarg, +emitted only when it differs from the default ``api_version``. """ import pytest @@ -130,9 +130,9 @@ def _serializer(code_model): return OperationSerializer(code_model, async_mode=False, client_namespace="blah") -def test_emits_client_api_version_getter_when_param_is_not_api_version(code_model): +def test_emits_client_api_version_name_when_param_is_not_api_version(code_model): # Storage-like: the versioning parameter is named ``version`` -> config attr - # is ``self.version``, so the decorator must read that attribute directly. + # is ``self.version``, so the decorator must be told to read that attribute. client = _client(code_model) client.config.parameters.parameters.append( _api_version_config_parameter(code_model, "version") @@ -141,7 +141,7 @@ def test_emits_client_api_version_getter_when_param_is_not_api_version(code_mode decorator = _serializer(code_model)._api_version_validation(operation) - assert " client_api_version_getter=lambda config: config.version," in decorator + assert ' client_api_version_name="version",' in decorator def test_no_kwarg_when_param_is_conventional_api_version(code_model): @@ -156,7 +156,7 @@ def test_no_kwarg_when_param_is_conventional_api_version(code_model): decorator = _serializer(code_model)._api_version_validation(operation) assert decorator # decorator is still emitted (operation has addedOn) - assert "client_api_version_getter" not in decorator + assert "client_api_version_name" not in decorator def test_no_kwarg_when_no_api_version_param(code_model): @@ -166,4 +166,4 @@ def test_no_kwarg_when_no_api_version_param(code_model): decorator = _serializer(code_model)._api_version_validation(operation) assert decorator - assert "client_api_version_getter" not in decorator + assert "client_api_version_name" not in decorator From cab1998f413e1f08ae43f2bc0d81a35c0f9c8ad4 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 24 Jul 2026 13:58:29 -0700 Subject: [PATCH 7/8] Bake api-version config attribute name into _validation.py Render config. directly instead of threading a decorator kwarg; resolve the api-version attribute name once at package level from ConfigParameter.client_name. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e983f91-eed5-4191-b531-0eca9e696313 --- ...rsion-validation-attr-2026-7-24-11-30-0.md | 2 +- .../codegen/serializers/builder_serializer.py | 6 - .../codegen/serializers/general_serializer.py | 22 ++- .../codegen/templates/validation.py.jinja2 | 3 +- .../tests/unit/test_api_version_validation.py | 141 +++++++----------- 5 files changed, 79 insertions(+), 95 deletions(-) diff --git a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md index 6ee21c24afe..449eb014807 100644 --- a/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md +++ b/.chronus/changes/fix-api-version-validation-attr-2026-7-24-11-30-0.md @@ -4,4 +4,4 @@ packages: - "@typespec/http-client-python" --- -Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. It previously hardcoded `client._config.api_version`, but the attribute name is derived from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. `self.version`), the lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now passes the real attribute name to the decorator via a `client_api_version_name` kwarg. +Fix the generated `_validation.py` `@api_version_validation` decorator so it reads the correct client config attribute for the API version. It previously hardcoded `client._config.api_version`, but the attribute name is derived from the API-version parameter's `client_name`. For specs that name the versioning parameter something other than `apiVersion` (e.g. `self.version`), the lookup raised `AttributeError` that the decorator silently swallowed, disabling all API-version validation for those clients. The emitter now bakes the real attribute name into the generated decorator so it reads `config.` directly. diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py index 9020b98f40a..b1da889bed6 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/builder_serializer.py @@ -632,12 +632,6 @@ def _api_version_validation(self, builder: OperationType) -> str: if retval: if builder.api_versions: retval.append(f" api_versions_list={builder.api_versions},") - api_version_param = next( - (p for p in builder.client.config.parameters.parameters if p.is_api_version), - None, - ) - if api_version_param and api_version_param.client_name != "api_version": - retval.append(f' client_api_version_name="{api_version_param.client_name}",') retval_str = "\n".join(retval) return f"@api_version_validation(\n{retval_str}\n)" return "" diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py index c55dc359c65..d44dbc8bc02 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/general_serializer.py @@ -320,7 +320,27 @@ def serialize_model_base_file(self) -> str: def serialize_validation_file(self) -> str: template = self.env.get_template("validation.py.jinja2") - return template.render(code_model=self.code_model) + return template.render( + code_model=self.code_model, + client_api_version_name=self._api_version_config_attr_name(), + ) + + def _api_version_config_attr_name(self) -> str: + """Name of the config attribute that stores the API version. + + The attribute name is derived from the API-version parameter's ``client_name`` + (e.g. ``apiVersion`` -> ``api_version``, or Storage's ``version`` -> ``version``). + ``_validation.py`` holds a single shared decorator for the whole package, so when + every client agrees on the name we bake it in directly; otherwise we fall back to + the conventional ``api_version``. + """ + names = { + parameter.client_name + for client in self.code_model.clients + for parameter in client.config.parameters.parameters + if parameter.is_api_version + } + return next(iter(names)) if len(names) == 1 else "api_version" def serialize_cross_language_definition_file(self) -> str: cross_langauge_def_dict = { diff --git a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 index e02867acf0b..62704009957 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 @@ -7,7 +7,6 @@ def api_version_validation(**kwargs): params_added_on = kwargs.pop("params_added_on", {}) method_added_on = kwargs.pop("method_added_on", "") api_versions_list = kwargs.pop("api_versions_list", []) - client_api_version_name = kwargs.pop("client_api_version_name", "api_version") def _index_with_default(value: str, default: int = -1) -> int: """Get the index of value in lst, or return default if not found. @@ -31,7 +30,7 @@ def api_version_validation(**kwargs): # this assumes the client has an _api_version attribute client = args[0] config = client._config # pylint: disable=protected-access - client_api_version = getattr(config, client_api_version_name) + client_api_version = config.{{ client_api_version_name }} except AttributeError: return func(*args, **kwargs) diff --git a/packages/http-client-python/tests/unit/test_api_version_validation.py b/packages/http-client-python/tests/unit/test_api_version_validation.py index edf4addbc12..3f024dfd912 100644 --- a/packages/http-client-python/tests/unit/test_api_version_validation.py +++ b/packages/http-client-python/tests/unit/test_api_version_validation.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -"""Tests for the ``@api_version_validation`` decorator emission. +"""Tests for the generated ``_validation.py`` ``@api_version_validation`` decorator. Regression coverage for the bug where the generated ``_validation.py`` decorator hardcoded ``client._config.api_version``. When the versioning parameter is named @@ -13,27 +13,19 @@ ``AttributeError`` which the decorator silently swallowed, disabling ALL api-version validation for those clients. -The fix threads the real config attribute name (the api-version parameter's -``client_name``) into the decorator via a ``client_api_version_name`` kwarg, -emitted only when it differs from the default ``api_version``. +The fix bakes the real config attribute name (the api-version parameter's +``client_name``) into the rendered ``_validation.py`` so the decorator reads +``config.`` directly. """ import pytest +from jinja2 import Environment, PackageLoader -from pygen.codegen.models import ( - Client, - CodeModel, - Operation, - ParameterList, - RequestBuilder, -) +from pygen.codegen.models import Client, CodeModel from pygen.codegen.models.parameter import ConfigParameter -from pygen.codegen.models.parameter_list import ( - ClientGlobalParameterList, - RequestBuilderParameterList, -) +from pygen.codegen.models.parameter_list import ClientGlobalParameterList from pygen.codegen.models.primitive_types import StringType -from pygen.codegen.serializers.builder_serializer import OperationSerializer +from pygen.codegen.serializers.general_serializer import GeneralSerializer @pytest.fixture @@ -58,22 +50,19 @@ def code_model(): "show-operations": True, "models-mode": "dpg", "version-tolerant": True, + "flavor": "azure", }, ) -def _client(code_model): - return Client( - { - "name": "client", - "namespace": "blah", - "moduleName": "blah", - "parameters": [], - "url": "", - "operationGroups": [], - }, - code_model, - parameters=ClientGlobalParameterList({}, code_model, parameters=[]), +def _env() -> Environment: + return Environment( + loader=PackageLoader("pygen.codegen", "templates"), + keep_trailing_newline=True, + line_statement_prefix="##", + line_comment_prefix="###", + trim_blocks=True, + lstrip_blocks=True, ) @@ -93,77 +82,59 @@ def _api_version_config_parameter(code_model, client_name: str) -> ConfigParamet ) -def _operation(code_model, client): - request_builder = RequestBuilder( - yaml_data={ - "url": "http://fake.com", - "method": "GET", - "groupName": "blah", - "isOverload": False, - "apiVersions": ["2023-01-01"], - }, - client=client, - code_model=code_model, - name="do_thing_request", - parameters=RequestBuilderParameterList({}, code_model, parameters=[]), - ) - return Operation( - yaml_data={ - "url": "http://fake.com", - "method": "GET", - "groupName": "blah", - "isOverload": False, - "apiVersions": ["2023-01-01"], - "addedOn": "2023-01-01", +def _client_with_api_version(code_model, client_name): + client = Client( + { + "name": "client", + "namespace": "blah", + "moduleName": "blah", + "parameters": [], + "url": "", + "operationGroups": [], }, - client=client, - code_model=code_model, - request_builder=request_builder, - name="do_thing", - parameters=ParameterList({}, code_model, parameters=[]), - responses=[], - exceptions=[], + code_model, + parameters=ClientGlobalParameterList({}, code_model, parameters=[]), ) + if client_name is not None: + client.config.parameters.parameters.append(_api_version_config_parameter(code_model, client_name)) + code_model.clients = [client] + return client -def _serializer(code_model): - return OperationSerializer(code_model, async_mode=False, client_namespace="blah") +def _render(code_model) -> str: + return GeneralSerializer(code_model=code_model, env=_env()).serialize_validation_file() -def test_emits_client_api_version_name_when_param_is_not_api_version(code_model): +def test_reads_version_attribute_for_non_conventional_param(code_model): # Storage-like: the versioning parameter is named ``version`` -> config attr - # is ``self.version``, so the decorator must be told to read that attribute. - client = _client(code_model) - client.config.parameters.parameters.append( - _api_version_config_parameter(code_model, "version") - ) - operation = _operation(code_model, client) + # is ``self.version``, so the decorator must read ``config.version`` directly. + _client_with_api_version(code_model, "version") - decorator = _serializer(code_model)._api_version_validation(operation) + rendered = _render(code_model) - assert ' client_api_version_name="version",' in decorator + assert "client_api_version = config.version" in rendered + assert "config.api_version" not in rendered + assert "getattr(" not in rendered -def test_no_kwarg_when_param_is_conventional_api_version(code_model): - # Conventional ``apiVersion`` -> config attr is ``self.api_version`` which is - # the decorator default, so no kwarg should be emitted (keeps regen churn low). - client = _client(code_model) - client.config.parameters.parameters.append( - _api_version_config_parameter(code_model, "api_version") - ) - operation = _operation(code_model, client) +def test_reads_api_version_attribute_for_conventional_param(code_model): + _client_with_api_version(code_model, "api_version") + + rendered = _render(code_model) + + assert "client_api_version = config.api_version" in rendered + - decorator = _serializer(code_model)._api_version_validation(operation) +def test_defaults_to_api_version_when_no_api_version_param(code_model): + _client_with_api_version(code_model, None) - assert decorator # decorator is still emitted (operation has addedOn) - assert "client_api_version_name" not in decorator + rendered = _render(code_model) + assert "client_api_version = config.api_version" in rendered -def test_no_kwarg_when_no_api_version_param(code_model): - client = _client(code_model) - operation = _operation(code_model, client) - decorator = _serializer(code_model)._api_version_validation(operation) +def test_api_version_config_attr_name_helper(code_model): + _client_with_api_version(code_model, "version") + serializer = GeneralSerializer(code_model=code_model, env=_env()) - assert decorator - assert "client_api_version_name" not in decorator + assert serializer._api_version_config_attr_name() == "version" From 3d6b98d492cc1d6585c1a099cad757f8c9b47745 Mon Sep 17 00:00:00 2001 From: Libba Lawrence Date: Fri, 24 Jul 2026 14:00:39 -0700 Subject: [PATCH 8/8] Inline protected _config access in _validation.py decorator Read client._config. directly; the baked-in short attribute name keeps the line under black's limit so the protected-access suppression stays attached. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9e983f91-eed5-4191-b531-0eca9e696313 --- .../generator/pygen/codegen/templates/validation.py.jinja2 | 3 +-- .../tests/unit/test_api_version_validation.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 index 62704009957..b7f70f0a6f2 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/validation.py.jinja2 @@ -29,8 +29,7 @@ def api_version_validation(**kwargs): try: # this assumes the client has an _api_version attribute client = args[0] - config = client._config # pylint: disable=protected-access - client_api_version = config.{{ client_api_version_name }} + client_api_version = client._config.{{ client_api_version_name }} # pylint: disable=protected-access except AttributeError: return func(*args, **kwargs) diff --git a/packages/http-client-python/tests/unit/test_api_version_validation.py b/packages/http-client-python/tests/unit/test_api_version_validation.py index 3f024dfd912..39ad9a0e7c4 100644 --- a/packages/http-client-python/tests/unit/test_api_version_validation.py +++ b/packages/http-client-python/tests/unit/test_api_version_validation.py @@ -112,7 +112,7 @@ def test_reads_version_attribute_for_non_conventional_param(code_model): rendered = _render(code_model) - assert "client_api_version = config.version" in rendered + assert "client_api_version = client._config.version" in rendered assert "config.api_version" not in rendered assert "getattr(" not in rendered @@ -122,7 +122,7 @@ def test_reads_api_version_attribute_for_conventional_param(code_model): rendered = _render(code_model) - assert "client_api_version = config.api_version" in rendered + assert "client_api_version = client._config.api_version" in rendered def test_defaults_to_api_version_when_no_api_version_param(code_model): @@ -130,7 +130,7 @@ def test_defaults_to_api_version_when_no_api_version_param(code_model): rendered = _render(code_model) - assert "client_api_version = config.api_version" in rendered + assert "client_api_version = client._config.api_version" in rendered def test_api_version_config_attr_name_helper(code_model):