Skip to content

Commit bf35b41

Browse files
l0lawrenceCopilot
andcommitted
fix(http-client-python): emit absolute import when namespaces share no package root
Structured JSONL/SSE streaming (and any cross-root model reference) could emit a package-escaping relative import such as `from .......search import models` when a payload model's client_namespace shares no top-level package component with the generated module's namespace (e.g. a `search` model referenced from an `azure.search.documents` package). Python rejects that at runtime with "attempted relative import beyond top-level package". CodeModel.get_relative_import_path now falls back to a valid absolute import when the common-prefix length is zero (idx == 0); in-package imports (which always share the package root, idx >= 1) are byte-identical to before. Stream/AsyncStream continue to be imported locally from `_utils.streaming_base`. Adds spec-agnostic regression tests for the helper. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e57edafe-9764-4b99-a1ae-0efd56e8729e
1 parent f5db637 commit bf35b41

2 files changed

Lines changed: 105 additions & 3 deletions

File tree

packages/http-client-python/generator/pygen/codegen/models/code_model.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,16 @@ def get_relative_import_path(
145145
if serialize_namespace_split[idx] != imported_namespace_split[idx]:
146146
break
147147
idx += 1
148-
self._relative_import_path[key] = "." * (len(serialize_namespace_split[idx:]) + 1) + ".".join(
149-
imported_namespace_split[idx:]
150-
)
148+
if idx == 0:
149+
# The two namespaces share no top-level package, so a relative import would
150+
# climb above the package root (e.g. ``from .......search import models``),
151+
# which Python rejects at runtime ("attempted relative import beyond top-level
152+
# package"). Emit a valid absolute import of the foreign namespace instead.
153+
self._relative_import_path[key] = ".".join(imported_namespace_split)
154+
else:
155+
self._relative_import_path[key] = "." * (len(serialize_namespace_split[idx:]) + 1) + ".".join(
156+
imported_namespace_split[idx:]
157+
)
151158
result = self._relative_import_path[key]
152159
if module_name is None:
153160
return result
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
"""Tests for ``CodeModel.get_relative_import_path``.
7+
8+
A structured (JSONL / SSE) streaming operation can reference a payload model
9+
whose ``client_namespace`` shares no top-level package with the operation being
10+
generated (for example an SSE event payload that TCGC reports under a ``search``
11+
namespace while the package root is ``azure.search.documents``). Emitting a
12+
relative import in that case climbs above the package root
13+
(``from .......search import models``), which Python rejects at runtime with
14+
``attempted relative import beyond top-level package``. The generator must fall
15+
back to a valid absolute import instead. In-package imports must stay relative
16+
(byte-identical to before).
17+
"""
18+
19+
import pytest
20+
21+
from pygen.codegen.models import CodeModel
22+
23+
24+
def _code_model() -> CodeModel:
25+
return CodeModel(
26+
{
27+
"clients": [
28+
{
29+
"name": "client",
30+
"namespace": "azure.search.documents",
31+
"moduleName": "azure.search.documents",
32+
"parameters": [],
33+
"url": "",
34+
"operationGroups": [],
35+
}
36+
],
37+
"namespace": "azure.search.documents",
38+
},
39+
options={
40+
"show-send-request": True,
41+
"builders-visibility": "public",
42+
"show-operations": True,
43+
"models-mode": "dpg",
44+
"only-path-and-body-params-positional": True,
45+
},
46+
)
47+
48+
49+
@pytest.mark.parametrize(
50+
"serialize_namespace,imported_namespace,expected",
51+
[
52+
# In-package: shares a top-level package -> relative import (unchanged).
53+
("azure.test.operations", "azure.test", ".."),
54+
("azure.test.operations", "azure", "..."),
55+
("azure.test.subtest.aio.operations", "azure.test", "...."),
56+
("azure.search.documents.operations", "azure.search.documents", ".."),
57+
],
58+
)
59+
def test_in_package_stays_relative(serialize_namespace, imported_namespace, expected):
60+
assert _code_model().get_relative_import_path(serialize_namespace, imported_namespace) == expected
61+
62+
63+
@pytest.mark.parametrize(
64+
"serialize_namespace,imported_namespace,expected",
65+
[
66+
# No shared top-level package -> valid absolute import (no leading dots).
67+
("azure.search.documents.grp.aio.operations", "search", "search"),
68+
("azure.search.documents.aio.operations", "search", "search"),
69+
("search.aio.operations", "azure.search.documents", "azure.search.documents"),
70+
],
71+
)
72+
def test_cross_root_uses_absolute_import(serialize_namespace, imported_namespace, expected):
73+
result = _code_model().get_relative_import_path(serialize_namespace, imported_namespace)
74+
assert result == expected
75+
assert not result.startswith("."), "cross-root import must be absolute, not a package-escaping relative import"
76+
77+
78+
def test_cross_root_absolute_import_with_module_name():
79+
# The Stream / AsyncStream import points at ``<root>._utils.streaming_base``. When an
80+
# operation lives outside the package root, that import must also be absolute-valid.
81+
result = _code_model().get_relative_import_path(
82+
"search.aio.operations",
83+
"azure.search.documents",
84+
module_name="_utils.streaming_base",
85+
)
86+
assert result == "azure.search.documents._utils.streaming_base"
87+
88+
89+
def test_in_package_module_name_stays_relative():
90+
result = _code_model().get_relative_import_path(
91+
"azure.search.documents.grp.aio.operations",
92+
"azure.search.documents",
93+
module_name="_utils.streaming_base",
94+
)
95+
assert result == "...._utils.streaming_base"

0 commit comments

Comments
 (0)