diff --git a/.chronus/changes/python-encode-boolean-test-2026-6-24-23-0-0.md b/.chronus/changes/python-encode-boolean-test-2026-6-24-23-0-0.md new file mode 100644 index 00000000000..1ab68d0911b --- /dev/null +++ b/.chronus/changes/python-encode-boolean-test-2026-6-24-23-0-0.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client-python" +--- + +Add mock API test coverage for `@encode(string)` on boolean properties (`encode/boolean` Spector scenarios). Fix Python generator to correctly serialize and deserialize boolean values encoded as strings (case-insensitive `true`/`false`). diff --git a/packages/http-client-python/eng/scripts/ci/regenerate-common.ts b/packages/http-client-python/eng/scripts/ci/regenerate-common.ts index 9b2defd0e8c..81330d54c2f 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate-common.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate-common.ts @@ -178,6 +178,9 @@ export const AZURE_EMITTER_OPTIONS: Record< "client/overload": { namespace: "client.overload", }, + "encode/boolean": { + namespace: "encode.boolean", + }, "encode/duration": { namespace: "encode.duration", }, diff --git a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py index cc864cf3ee1..9d231548d47 100644 --- a/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py +++ b/packages/http-client-python/generator/pygen/codegen/models/primitive_types.py @@ -49,6 +49,11 @@ def default_template_representation_declaration(self) -> str: class BooleanType(PrimitiveType): + def __init__(self, yaml_data: dict[str, Any], code_model: "CodeModel") -> None: + super().__init__(yaml_data=yaml_data, code_model=code_model) + if yaml_data.get("encode") == "string": + self.encode = "str" + def serialization_type(self, **kwargs: Any) -> str: return "bool" 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 e51a7645dde..dbe8f8f0adb 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 @@ -348,6 +348,12 @@ def _deserialize_int_as_str(attr): return int(attr) +def _deserialize_bool_as_str(attr): + if isinstance(attr, bool): + return attr + return attr.lower() == "true" + + _DESERIALIZE_MAPPING = { datetime: _deserialize_datetime, date: _deserialize_date, @@ -375,6 +381,8 @@ _DESERIALIZE_MAPPING_WITHFORMAT = { def get_deserializer(annotation: typing.Any, rf: typing.Optional["_RestField"] = None): if annotation is int and rf and rf._format == "str": return _deserialize_int_as_str + if annotation is bool and rf and rf._format == "str": + return _deserialize_bool_as_str if annotation is str and rf and rf._format in _ARRAY_ENCODE_MAPPING: return functools.partial(_deserialize_array_encoded, _ARRAY_ENCODE_MAPPING[rf._format]) if rf and rf._format: diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index 01d5631147f..9c78f12ccf6 100644 --- a/packages/http-client-python/package-lock.json +++ b/packages/http-client-python/package-lock.json @@ -29,7 +29,7 @@ "@typespec/compiler": "^1.13.0", "@typespec/events": "~0.83.0", "@typespec/http": "^1.13.0", - "@typespec/http-specs": "0.1.0-alpha.39-dev.2", + "@typespec/http-specs": "0.1.0-alpha.39-dev.4", "@typespec/openapi": "^1.13.0", "@typespec/rest": "~0.83.0", "@typespec/spec-api": "0.1.0-alpha.14", @@ -2529,20 +2529,20 @@ } }, "node_modules/@typespec/http-specs": { - "version": "0.1.0-alpha.39-dev.2", - "resolved": "https://registry.npmjs.org/@typespec/http-specs/-/http-specs-0.1.0-alpha.39-dev.2.tgz", - "integrity": "sha512-NsxuimkS12gB6RF3j2yGf48bV/LzG0JQbtrZbiGy00RxOAZV59L7gYp852ij6fYN37Uv8P31KHcGM6wIAduAyg==", + "version": "0.1.0-alpha.39-dev.4", + "resolved": "https://registry.npmjs.org/@typespec/http-specs/-/http-specs-0.1.0-alpha.39-dev.4.tgz", + "integrity": "sha512-kcbEa1n/3L+4ed6BFTuGZWp9w79q69lC2awvQ5oDeP9mSOE2hzorszlby0t6h4k4pplucAVinNi26LJeOfmUzA==", "dev": true, "license": "MIT", "dependencies": { - "@typespec/spec-api": "^0.1.0-alpha.14 || >= 0.1.0-dev.0", - "@typespec/spector": "^0.1.0-alpha.25 || >= 0.1.0-dev.0" + "@typespec/spec-api": "^0.1.0-alpha.14 || >= 0.1.0-alpha.15-dev.1", + "@typespec/spector": "^0.1.0-alpha.25 || >= 0.1.0-alpha.26-dev.1" }, "engines": { "node": ">=22.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.13.0 || >= 1.14.0-dev.0", + "@typespec/compiler": "^1.13.0 || >= 1.14.0-dev.5", "@typespec/http": "^1.13.0 || >= 1.14.0-dev.0", "@typespec/rest": "^0.83.0 || >= 0.84.0-dev.0", "@typespec/versioning": "^0.83.0 || >= 0.84.0-dev.0", diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index 65f08273ff0..bd3f22af688 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -119,7 +119,7 @@ "@typespec/sse": "~0.83.0", "@typespec/streams": "~0.83.0", "@typespec/xml": "~0.83.0", - "@typespec/http-specs": "0.1.0-alpha.39-dev.2", + "@typespec/http-specs": "0.1.0-alpha.39-dev.4", "@types/js-yaml": "~4.0.5", "@types/node": "~25.0.2", "@types/semver": "7.5.8", diff --git a/packages/http-client-python/tests/mock_api/azure/asynctests/test_encode_boolean_async.py b/packages/http-client-python/tests/mock_api/azure/asynctests/test_encode_boolean_async.py new file mode 100644 index 00000000000..3245fdb434e --- /dev/null +++ b/packages/http-client-python/tests/mock_api/azure/asynctests/test_encode_boolean_async.py @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import pytest +import pytest_asyncio +from encode.boolean.aio import BooleanClient +from encode.boolean import models + + +@pytest_asyncio.fixture +async def client(): + async with BooleanClient() as client: + yield client + + +@pytest.mark.asyncio +async def test_property_true_lower(client: BooleanClient): + result = await client.property.true_lower(models.BoolAsStringProperty(value=True)) + assert result.value is True + assert result["value"] == "true" + + +@pytest.mark.asyncio +async def test_property_false_lower(client: BooleanClient): + result = await client.property.false_lower(models.BoolAsStringProperty(value=False)) + assert result.value is False + assert result["value"] == "false" + + +@pytest.mark.asyncio +async def test_property_true_upper(client: BooleanClient): + result = await client.property.true_upper(models.BoolAsStringProperty(value=True)) + assert result.value is True + assert result["value"] == "TRUE" + + +@pytest.mark.asyncio +async def test_property_false_mixed(client: BooleanClient): + result = await client.property.false_mixed(models.BoolAsStringProperty(value=False)) + assert result.value is False + assert result["value"] == "FaLsE" diff --git a/packages/http-client-python/tests/mock_api/azure/test_encode_boolean.py b/packages/http-client-python/tests/mock_api/azure/test_encode_boolean.py new file mode 100644 index 00000000000..28a89fed980 --- /dev/null +++ b/packages/http-client-python/tests/mock_api/azure/test_encode_boolean.py @@ -0,0 +1,37 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import pytest +from encode.boolean import BooleanClient, models + + +@pytest.fixture +def client(): + with BooleanClient() as client: + yield client + + +def test_property_true_lower(client: BooleanClient): + result = client.property.true_lower(models.BoolAsStringProperty(value=True)) + assert result.value is True + assert result["value"] == "true" + + +def test_property_false_lower(client: BooleanClient): + result = client.property.false_lower(models.BoolAsStringProperty(value=False)) + assert result.value is False + assert result["value"] == "false" + + +def test_property_true_upper(client: BooleanClient): + result = client.property.true_upper(models.BoolAsStringProperty(value=True)) + assert result.value is True + assert result["value"] == "TRUE" + + +def test_property_false_mixed(client: BooleanClient): + result = client.property.false_mixed(models.BoolAsStringProperty(value=False)) + assert result.value is False + assert result["value"] == "FaLsE" diff --git a/packages/http-client-python/tests/mock_api/unbranded/asynctests/test_encode_boolean_async.py b/packages/http-client-python/tests/mock_api/unbranded/asynctests/test_encode_boolean_async.py new file mode 100644 index 00000000000..7083622bb99 --- /dev/null +++ b/packages/http-client-python/tests/mock_api/unbranded/asynctests/test_encode_boolean_async.py @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import pytest +import pytest_asyncio +from encode.boolean.aio import BooleanClient +from encode.boolean.property import models + + +@pytest_asyncio.fixture +async def client(): + async with BooleanClient() as client: + yield client + + +@pytest.mark.asyncio +async def test_property_true_lower(client: BooleanClient): + result = await client.property.true_lower(models.BoolAsStringProperty(value=True)) + assert result.value is True + assert result["value"] == "true" + + +@pytest.mark.asyncio +async def test_property_false_lower(client: BooleanClient): + result = await client.property.false_lower(models.BoolAsStringProperty(value=False)) + assert result.value is False + assert result["value"] == "false" + + +@pytest.mark.asyncio +async def test_property_true_upper(client: BooleanClient): + result = await client.property.true_upper(models.BoolAsStringProperty(value=True)) + assert result.value is True + assert result["value"] == "TRUE" + + +@pytest.mark.asyncio +async def test_property_false_mixed(client: BooleanClient): + result = await client.property.false_mixed(models.BoolAsStringProperty(value=False)) + assert result.value is False + assert result["value"] == "FaLsE" diff --git a/packages/http-client-python/tests/mock_api/unbranded/test_encode_boolean.py b/packages/http-client-python/tests/mock_api/unbranded/test_encode_boolean.py new file mode 100644 index 00000000000..0d98ed6416f --- /dev/null +++ b/packages/http-client-python/tests/mock_api/unbranded/test_encode_boolean.py @@ -0,0 +1,38 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import pytest +from encode.boolean import BooleanClient +from encode.boolean.property import models + + +@pytest.fixture +def client(): + with BooleanClient() as client: + yield client + + +def test_property_true_lower(client: BooleanClient): + result = client.property.true_lower(models.BoolAsStringProperty(value=True)) + assert result.value is True + assert result["value"] == "true" + + +def test_property_false_lower(client: BooleanClient): + result = client.property.false_lower(models.BoolAsStringProperty(value=False)) + assert result.value is False + assert result["value"] == "false" + + +def test_property_true_upper(client: BooleanClient): + result = client.property.true_upper(models.BoolAsStringProperty(value=True)) + assert result.value is True + assert result["value"] == "TRUE" + + +def test_property_false_mixed(client: BooleanClient): + result = client.property.false_mixed(models.BoolAsStringProperty(value=False)) + assert result.value is False + assert result["value"] == "FaLsE"