diff --git a/.chronus/changes/preserve-pyproject-fields-2026-06-15.md b/.chronus/changes/preserve-pyproject-fields-2026-06-15.md new file mode 100644 index 00000000000..3642556247f --- /dev/null +++ b/.chronus/changes/preserve-pyproject-fields-2026-06-15.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Preserve manually customized `description`, `classifiers`, and `[project.urls]` fields in an existing `pyproject.toml` instead of overwriting them on regeneration. 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 6c022c9259b..d94282ac662 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 @@ -85,6 +85,13 @@ def external_lib_version_map(self, file_content: str, additional_version_map: di # Process dependencies if "project" in loaded_pyproject_toml: + project = loaded_pyproject_toml["project"] + + # Keep manually customized project fields the emitter would otherwise overwrite. + for field in ("description", "classifiers", "urls"): + if field in project: + result["KEEP_FIELDS"][f"project.{field}"] = project[field] + # Handle main dependencies if "dependencies" in loaded_pyproject_toml["project"]: kept_deps = [] diff --git a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 index 4a012849501..851cc641b3a 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 @@ -14,12 +14,21 @@ name = "{{ options.get('package-name')|lower }}" authors = [ { name = "{{ code_model.company_name }}"{% if code_model.is_azure_flavor %}, email = "azpysdkhelp@microsoft.com"{% endif %} }, ] -{% if options.get("azure-arm") %} +{% if KEEP_FIELDS and KEEP_FIELDS.get('project.description') %} +description = {{ KEEP_FIELDS.get('project.description')|tojson }} +{% elif options.get("azure-arm") %} description = "Microsoft Azure {{ options.get('package-pprint-name') }} Client Library for Python" {% else %} description = "{{ code_model.company_name }} {% if code_model.is_azure_flavor and not options.get('package-pprint-name').startswith('Azure ') %}Azure {% endif %}{{ options.get('package-pprint-name') }} Client Library for Python" {% endif %} license = "MIT" +{% if KEEP_FIELDS and KEEP_FIELDS.get('project.classifiers') %} +classifiers = [ + {% for classifier in KEEP_FIELDS.get('project.classifiers') %} + {{ classifier|tojson }}, + {% endfor %} +] +{% else %} classifiers = [ "Development Status :: {{ dev_status }}", "Programming Language :: Python", @@ -29,10 +38,15 @@ classifiers = [ "Programming Language :: Python :: 3.{{ version }}", {% endfor %} ] +{% endif %} requires-python = ">={{ MIN_PYTHON_VERSION }}" {% else %} +{% if KEEP_FIELDS and KEEP_FIELDS.get('project.description') %} +description = {{ KEEP_FIELDS.get('project.description')|tojson }} +{% else %} description = "{{ options.get('package-name') }}" {% endif %} +{% endif %} {% if code_model.is_azure_flavor %} keywords = ["azure", "azure sdk"] {% endif %} @@ -77,7 +91,13 @@ version = "{{ options.get("package-version", "unknown") }}" ] {% endfor %} {% endif %} -{% if code_model.is_azure_flavor %} +{% if KEEP_FIELDS and KEEP_FIELDS.get('project.urls') %} + +[project.urls] +{% for key, val in KEEP_FIELDS.get('project.urls').items() %} +{{ key }} = {{ val|tojson }} +{% endfor %} +{% elif code_model.is_azure_flavor %} [project.urls] repository = "https://github.com/Azure/azure-sdk-for-python" diff --git a/packages/http-client-python/tests/unit/test_pyproject_keep_fields.py b/packages/http-client-python/tests/unit/test_pyproject_keep_fields.py new file mode 100644 index 00000000000..b52a8a4f4a8 --- /dev/null +++ b/packages/http-client-python/tests/unit/test_pyproject_keep_fields.py @@ -0,0 +1,72 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +"""Unit tests for preserving manually customized pyproject.toml fields. + +The emitter regenerates pyproject.toml on every emit. Manual edits to fields +the emitter doesn't own (description, classifiers, project URLs) must be +preserved so they are not clobbered (see GitHub issue #10311). +""" +from pygen.codegen.serializers.general_serializer import GeneralSerializer + + +def _keep_fields(file_content: str) -> dict: + # external_lib_version_map only relies on module-level helpers, not on + # instance state, so we can bypass __init__ for a focused unit test. + serializer = GeneralSerializer.__new__(GeneralSerializer) + return serializer.external_lib_version_map(file_content, {})["KEEP_FIELDS"] + + +def test_preserve_description(): + content = """ +[project] +name = "azure-ai-sample" +description = "Microsoft Azure AI Sample Client Library for Python" +""" + keep_fields = _keep_fields(content) + assert keep_fields["project.description"] == "Microsoft Azure AI Sample Client Library for Python" + + +def test_preserve_classifiers(): + content = """ +[project] +name = "azure-ai-sample" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] +""" + keep_fields = _keep_fields(content) + assert "Programming Language :: Python :: 3.14" in keep_fields["project.classifiers"] + + +def test_preserve_project_urls(): + content = """ +[project] +name = "azure-ai-sample" + +[project.urls] +repository = "https://github.com/Azure/azure-sdk-for-python-custom" +documentation = "https://aka.ms/custom-docs" +""" + keep_fields = _keep_fields(content) + assert keep_fields["project.urls"]["repository"] == "https://github.com/Azure/azure-sdk-for-python-custom" + assert keep_fields["project.urls"]["documentation"] == "https://aka.ms/custom-docs" + + +def test_missing_fields_not_kept(): + content = """ +[project] +name = "azure-ai-sample" +""" + keep_fields = _keep_fields(content) + assert "project.description" not in keep_fields + assert "project.classifiers" not in keep_fields + assert "project.urls" not in keep_fields + + +def test_invalid_toml_returns_empty(): + assert _keep_fields("this is : not valid = toml [[[") == {}