From f3c578bd82d41b2702a2090752894097b51d4818 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 3 Nov 2025 11:25:18 +0800 Subject: [PATCH 01/11] fix for typespec --- .../generator/pygen/codegen/serializers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py index 56e404397ee..e7654299885 100644 --- a/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py +++ b/packages/http-client-python/generator/pygen/codegen/serializers/__init__.py @@ -125,7 +125,7 @@ def keep_version_file(self) -> bool: # pylint: disable=too-many-branches def serialize(self) -> None: # remove existing folders when generate from tsp - if self.code_model.options.get("clear-output-folder"): + if self.code_model.is_tsp and self.code_model.options.get("clear-output-folder"): # remove generated_samples and generated_tests folder self.remove_folder(self._generated_tests_samples_folder("generated_samples")) self.remove_folder(self._generated_tests_samples_folder("generated_tests")) From 14fb13ca2ec5b488c976b8647f557e0ca5eb5b1e Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 3 Nov 2025 11:26:17 +0800 Subject: [PATCH 02/11] add changelog --- .chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md diff --git a/.chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md b/.chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md new file mode 100644 index 00000000000..45555c876aa --- /dev/null +++ b/.chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Avoid duplicated delete operation for autorest emitter \ No newline at end of file From 9c2bd37ec84a50ffb5db9a04940cace10a9fbbd2 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 3 Nov 2025 11:33:14 +0800 Subject: [PATCH 03/11] add changelog --- .chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md | 7 ------- packages/http-client-python/CHANGELOG.md | 7 +++++++ packages/http-client-python/package-lock.json | 4 ++-- packages/http-client-python/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) delete mode 100644 .chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md diff --git a/.chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md b/.chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md deleted file mode 100644 index 45555c876aa..00000000000 --- a/.chronus/changes/publish-python-11-03-2025-10-3-11-26-2.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/http-client-python" ---- - -Avoid duplicated delete operation for autorest emitter \ No newline at end of file diff --git a/packages/http-client-python/CHANGELOG.md b/packages/http-client-python/CHANGELOG.md index 33de54882de..add81c5f210 100644 --- a/packages/http-client-python/CHANGELOG.md +++ b/packages/http-client-python/CHANGELOG.md @@ -1,5 +1,12 @@ # Change Log - @typespec/http-client-python +## 0.20.2 + +### Bug Fixes + +- [#8905](https://github.com/microsoft/typespec/pull/8905) Avoid duplicated delete operation for autorest emitter + + ## 0.20.1 ### Bug Fixes diff --git a/packages/http-client-python/package-lock.json b/packages/http-client-python/package-lock.json index 7659f91633e..f5919e31cc8 100644 --- a/packages/http-client-python/package-lock.json +++ b/packages/http-client-python/package-lock.json @@ -1,12 +1,12 @@ { "name": "@typespec/http-client-python", - "version": "0.19.2", + "version": "0.20.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@typespec/http-client-python", - "version": "0.19.2", + "version": "0.20.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/packages/http-client-python/package.json b/packages/http-client-python/package.json index 6a256bacd35..da0fc0b6005 100644 --- a/packages/http-client-python/package.json +++ b/packages/http-client-python/package.json @@ -1,6 +1,6 @@ { "name": "@typespec/http-client-python", - "version": "0.20.1", + "version": "0.20.2", "author": "Microsoft Corporation", "description": "TypeSpec emitter for Python SDKs", "homepage": "https://typespec.io", From e20c340e223a55c9701d0763edc3f0da22d708a7 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 4 Nov 2025 13:06:15 +0800 Subject: [PATCH 04/11] do not add continuous blank lines --- .../generator/pygen/codegen/templates/macros.jinja2 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 index 06868e5d6ab..163ce0be55f 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 @@ -19,7 +19,10 @@ {%- set wrapped = line.strip() | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring=wrap_string) -%} {%- set _ = result_lines.append(wrapped) -%} {%- else -%} - {%- set _ = result_lines.append('') -%} + {# Do not add continuous blank lines #} + {%- if (result_lines and result_lines[-1] != '') or not result_lines -%} + {%- set _ = result_lines.append('') -%} + {%- endif -%} {%- endif -%} {%- endfor -%} {%- set original_result = result_lines | join('\n') -%} From 3456490ee4250729a5e66a50b1234584416c7459 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 4 Nov 2025 14:32:33 +0800 Subject: [PATCH 05/11] fix indent --- .../generator/pygen/codegen/templates/macros.jinja2 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 index 163ce0be55f..04adc8e0865 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 @@ -5,18 +5,19 @@ {% set enable_custom_handling = "\n* " in doc_string or doc_string.startswith("* ") %} {%- if enable_custom_handling -%} {%- set lines = doc_string.split('\n') -%} + {%- set base_indent = wrap_string.lstrip('\n') -%} {%- set result_lines = [] -%} {%- for line in lines -%} {%- if line.startswith('* ') -%} {# Handle bullet points with proper continuation alignment #} {%- set bullet_content = line[2:] -%} - {%- set base_indent = wrap_string.lstrip('\n') -%} {%- set bullet_line = base_indent + ' * ' + bullet_content -%} {%- set continuation_spaces = base_indent + ' ' -%} {%- set wrapped = bullet_line | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring='\n' + continuation_spaces) -%} {%- set _ = result_lines.append(wrapped) -%} {%- elif line.strip() -%} - {%- set wrapped = line.strip() | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring=wrap_string) -%} + {%- set line_indent = '' if line.strip().startswith(':') or loop.index == 1 else (base_indent + ' ') -%} + {%- set wrapped = (line_indent + line) | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring=wrap_string) -%} {%- set _ = result_lines.append(wrapped) -%} {%- else -%} {# Do not add continuous blank lines #} From 8fef5920213846a09ccd43b11c825bc4a9a7b02b Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 4 Nov 2025 15:30:41 +0800 Subject: [PATCH 06/11] fix indent for line break --- .../generator/pygen/codegen/templates/macros.jinja2 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 b/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 index 04adc8e0865..83ecabc5d15 100644 --- a/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 +++ b/packages/http-client-python/generator/pygen/codegen/templates/macros.jinja2 @@ -18,7 +18,10 @@ {%- elif line.strip() -%} {%- set line_indent = '' if line.strip().startswith(':') or loop.index == 1 else (base_indent + ' ') -%} {%- set wrapped = (line_indent + line) | wordwrap(width=95, break_long_words=False, break_on_hyphens=False, wrapstring=wrap_string) -%} - {%- set _ = result_lines.append(wrapped) -%} + {%- for line in wrapped.split('\n') -%} + {%- set prefix = "" if loop.index == 1 else " " -%} + {%- set _ = result_lines.append(prefix + line) -%} + {%- endfor -%} {%- else -%} {# Do not add continuous blank lines #} {%- if (result_lines and result_lines[-1] != '') or not result_lines -%} From 127f065384048e8791474c6186b92f1257796eac Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 4 Nov 2025 15:31:27 +0800 Subject: [PATCH 07/11] add changelog --- .../changes/python-fix-docstring-2025-10-4-15-31-14.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/python-fix-docstring-2025-10-4-15-31-14.md diff --git a/.chronus/changes/python-fix-docstring-2025-10-4-15-31-14.md b/.chronus/changes/python-fix-docstring-2025-10-4-15-31-14.md new file mode 100644 index 00000000000..6abfef34bc4 --- /dev/null +++ b/.chronus/changes/python-fix-docstring-2025-10-4-15-31-14.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-python" +--- + +Fix bad indent \ No newline at end of file From 46f310113a8f7261fec4205dd966c18266af0fc2 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 4 Nov 2025 16:30:14 +0800 Subject: [PATCH 08/11] add test case --- .../eng/scripts/ci/regenerate.ts | 25 ++++++++-- .../test/azure/specs/docstring/main.tsp | 38 +++++++++++++++ .../test/unittests/test_docstring.py | 46 +++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 packages/http-client-python/generator/test/azure/specs/docstring/main.tsp create mode 100644 packages/http-client-python/generator/test/unittests/test_docstring.py diff --git a/packages/http-client-python/eng/scripts/ci/regenerate.ts b/packages/http-client-python/eng/scripts/ci/regenerate.ts index b13ac4cd1da..516a0a48227 100644 --- a/packages/http-client-python/eng/scripts/ci/regenerate.ts +++ b/packages/http-client-python/eng/scripts/ci/regenerate.ts @@ -30,6 +30,7 @@ const PLUGIN_DIR = argv.values.pluginDir : resolve(fileURLToPath(import.meta.url), "../../../../"); const AZURE_HTTP_SPECS = resolve(PLUGIN_DIR, "node_modules/@azure-tools/azure-http-specs/specs"); const HTTP_SPECS = resolve(PLUGIN_DIR, "node_modules/@typespec/http-specs/specs"); +const LOCAL_AZURE_SPECS = resolve(PLUGIN_DIR, "generator/test/azure/specs"); const GENERATED_FOLDER = argv.values.generatedFolder ? resolve(argv.values.generatedFolder) : resolve(PLUGIN_DIR, "generator"); @@ -278,8 +279,25 @@ function toPosix(dir: string): string { return dir.replace(/\\/g, "/"); } +// Classify a spec path to determine its root and whether it should be treated as an Azure spec. +function classifySpec(spec: string): { specDir: string; isAzure: boolean } { + const posixSpec = toPosix(spec); + if (posixSpec.startsWith(toPosix(AZURE_HTTP_SPECS) + "/")) { + return { specDir: AZURE_HTTP_SPECS, isAzure: true }; + } + if (posixSpec.startsWith(toPosix(LOCAL_AZURE_SPECS) + "/")) { + // Local azure specs (in repo) should behave like azure specs for emitter options & naming. + return { specDir: LOCAL_AZURE_SPECS, isAzure: true }; + } + if (posixSpec.startsWith(toPosix(HTTP_SPECS) + "/")) { + return { specDir: HTTP_SPECS, isAzure: false }; + } + // Fallback: treat as non-azure and use HTTP_SPECS for relative path to avoid '..' segments. + return { specDir: HTTP_SPECS, isAzure: false }; +} + function getEmitterOption(spec: string, flavor: string): Record[] { - const specDir = spec.includes("azure") ? AZURE_HTTP_SPECS : HTTP_SPECS; + const { specDir } = classifySpec(spec); const relativeSpec = toPosix(relative(specDir, spec)); const key = relativeSpec.includes("resiliency/srv-driven/old.tsp") ? relativeSpec @@ -374,7 +392,7 @@ async function getSubdirectories(baseDir: string, flags: RegenerateFlags): Promi } function defaultPackageName(spec: string): string { - const specDir = spec.includes("azure") ? AZURE_HTTP_SPECS : HTTP_SPECS; + const { specDir } = classifySpec(spec); return toPosix(relative(specDir, dirname(spec))) .replace(/\//g, "-") .toLowerCase(); @@ -480,11 +498,12 @@ async function regenerate(flags: RegenerateFlagsInput): Promise { await preprocess(flags); const flagsResolved = { debug: false, flavor: flags.flavor, ...flags }; + const subdirectoriesForLocalAzure = await getSubdirectories(LOCAL_AZURE_SPECS, flagsResolved); const subdirectoriesForAzure = await getSubdirectories(AZURE_HTTP_SPECS, flagsResolved); const subdirectoriesForNonAzure = await getSubdirectories(HTTP_SPECS, flagsResolved); const subdirectories = flags.flavor === "azure" - ? [...subdirectoriesForAzure, ...subdirectoriesForNonAzure] + ? [...subdirectoriesForLocalAzure, ...subdirectoriesForAzure, ...subdirectoriesForNonAzure] : subdirectoriesForNonAzure; const cmdList: TspCommand[] = subdirectories.flatMap((subdirectory) => _getCmdList(subdirectory, flagsResolved), diff --git a/packages/http-client-python/generator/test/azure/specs/docstring/main.tsp b/packages/http-client-python/generator/test/azure/specs/docstring/main.tsp new file mode 100644 index 00000000000..5d914b4c229 --- /dev/null +++ b/packages/http-client-python/generator/test/azure/specs/docstring/main.tsp @@ -0,0 +1,38 @@ +import "@typespec/http"; + +using Http; + +@service(#{ title: "Doc Service" }) +namespace DocString; + +model DocModel { + @doc(""" + An array of tools the model may call while generating a response. + You can specify which tool to use by setting the `tool_choice` parameter. + + The two categories of tools you can provide the model are: + + - **Built-in tools**: Tools that are provided by OpenAI that extend the model's capabilities, like file search. + - **Function calls (custom tools)**: Functions that are defined by you, enabling the model to call your own code. + """) + doc1: string; + + @doc(""" + Specifies the processing type used for serving the request. + * If set to 'auto', then the request will be processed with the service tier configured in the Project settings. Unless otherwise configured, the Project will use 'default'. + * If set to 'default', then the request will be processed with the standard pricing and performance for the selected model. + * If set to '[flex](https://platform.openai.com/docs/guides/flex-processing)' or 'priority', then the request will be processed with the corresponding service tier. [Contact sales](https://openai.com/contact-sales) to learn more about Priority processing. + * When not set, the default behavior is 'auto'. + + When the `service_tier` parameter is set, the response body will include the `service_tier` + value based on the processing mode actually used to serve the request. This response value + may be different from the value set in the parameter. + """) + doc2: string; +} + +@route("/docstring") +interface DocString { + /** Get doc */ + @get get(): DocModel; +} diff --git a/packages/http-client-python/generator/test/unittests/test_docstring.py b/packages/http-client-python/generator/test/unittests/test_docstring.py new file mode 100644 index 00000000000..d607f88cb84 --- /dev/null +++ b/packages/http-client-python/generator/test/unittests/test_docstring.py @@ -0,0 +1,46 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +import hashlib +from pathlib import Path + +_FILE_PATH = Path(__file__) + + +def string_to_hash_id(input_string): + """ + Converts a string to a SHA256 hash ID. + + Args: + input_string (str): The string to be hashed. + + Returns: + str: The hexadecimal representation of the SHA256 hash. + """ + # Encode the string to bytes, which is required by hashlib + encoded_string = input_string.encode("utf-8") + + # Create a SHA256 hash object + hasher = hashlib.sha256() + + # Update the hash object with the encoded string + hasher.update(encoded_string) + + # Get the hexadecimal representation of the hash + hash_id = hasher.hexdigest() + + return hash_id + + +def test_docstring_generation(): + with open( + _FILE_PATH.parent.parent / "azure/generated/docstring/docstring/models/_models.py", "r", encoding="utf-8" + ) as f: + content = f.read() + hash_id = string_to_hash_id(content) + + # We expect there shall be no changes for each regeneration so that we could make sure generated docstring is stable. + # Of course, if there are intentional changes to docstring generation logic, we need to update the expected hash value accordingly. + assert hash_id == "fe6f89d00143221dcedfb4ce69440600099662aa0ecf933ab463c4d2518ba3d0" From a82ed10f509df707a8017cd4611e1069be0edb03 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 4 Nov 2025 17:11:27 +0800 Subject: [PATCH 09/11] add requirements for test case --- .../http-client-python/generator/test/azure/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/http-client-python/generator/test/azure/requirements.txt b/packages/http-client-python/generator/test/azure/requirements.txt index 9bd7d045774..fed3df503db 100644 --- a/packages/http-client-python/generator/test/azure/requirements.txt +++ b/packages/http-client-python/generator/test/azure/requirements.txt @@ -41,6 +41,7 @@ azure-mgmt-core==1.6.0 -e ./generated/client-structure-multiclient -e ./generated/client-structure-renamedoperation -e ./generated/client-structure-twooperationgroup +-e ./generated/docstring -e ./generated/resiliency-srv-driven1 -e ./generated/resiliency-srv-driven2 From c3771ebe0e28fd352de4f96e3ebbe1e0c76d5971 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 4 Nov 2025 10:12:32 +0000 Subject: [PATCH 10/11] fix ci --- .../generator/test/azure/specs/docstring/main.tsp | 2 +- .../generator/test/unittests/requirements.txt | 1 + .../generator/test/unittests/test_docstring.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/http-client-python/generator/test/azure/specs/docstring/main.tsp b/packages/http-client-python/generator/test/azure/specs/docstring/main.tsp index 5d914b4c229..28a57216d58 100644 --- a/packages/http-client-python/generator/test/azure/specs/docstring/main.tsp +++ b/packages/http-client-python/generator/test/azure/specs/docstring/main.tsp @@ -3,7 +3,7 @@ import "@typespec/http"; using Http; @service(#{ title: "Doc Service" }) -namespace DocString; +namespace Azure.DocString; model DocModel { @doc(""" diff --git a/packages/http-client-python/generator/test/unittests/requirements.txt b/packages/http-client-python/generator/test/unittests/requirements.txt index 9ef7ef8ab35..09c38f2cc3a 100644 --- a/packages/http-client-python/generator/test/unittests/requirements.txt +++ b/packages/http-client-python/generator/test/unittests/requirements.txt @@ -1,3 +1,4 @@ -r ../dev_requirements.txt -e ../../../generator -e ../unbranded/generated/special-words +-e ../azure/generated/docstring diff --git a/packages/http-client-python/generator/test/unittests/test_docstring.py b/packages/http-client-python/generator/test/unittests/test_docstring.py index d607f88cb84..d0ea7e07e6b 100644 --- a/packages/http-client-python/generator/test/unittests/test_docstring.py +++ b/packages/http-client-python/generator/test/unittests/test_docstring.py @@ -35,8 +35,10 @@ def string_to_hash_id(input_string): def test_docstring_generation(): + import azure.docstring + with open( - _FILE_PATH.parent.parent / "azure/generated/docstring/docstring/models/_models.py", "r", encoding="utf-8" + _FILE_PATH.parent.parent / "azure/generated/docstring/azure/docstring/models/_models.py", "r", encoding="utf-8" ) as f: content = f.read() hash_id = string_to_hash_id(content) From b79253aeee3fc716040e832e6473866a33f2e93b Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Wed, 5 Nov 2025 02:19:39 +0000 Subject: [PATCH 11/11] update --- .../http-client-python/generator/test/unittests/requirements.txt | 1 - .../generator/test/unittests/test_docstring.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/http-client-python/generator/test/unittests/requirements.txt b/packages/http-client-python/generator/test/unittests/requirements.txt index 09c38f2cc3a..9ef7ef8ab35 100644 --- a/packages/http-client-python/generator/test/unittests/requirements.txt +++ b/packages/http-client-python/generator/test/unittests/requirements.txt @@ -1,4 +1,3 @@ -r ../dev_requirements.txt -e ../../../generator -e ../unbranded/generated/special-words --e ../azure/generated/docstring diff --git a/packages/http-client-python/generator/test/unittests/test_docstring.py b/packages/http-client-python/generator/test/unittests/test_docstring.py index d0ea7e07e6b..4a978d2e667 100644 --- a/packages/http-client-python/generator/test/unittests/test_docstring.py +++ b/packages/http-client-python/generator/test/unittests/test_docstring.py @@ -35,7 +35,6 @@ def string_to_hash_id(input_string): def test_docstring_generation(): - import azure.docstring with open( _FILE_PATH.parent.parent / "azure/generated/docstring/azure/docstring/models/_models.py", "r", encoding="utf-8"