Skip to content
This repository was archived by the owner on May 22, 2026. It is now read-only.

Commit 4592053

Browse files
authored
sync eng change of microsoft/typespec#10323 (#3462)
* sync structure diff * add changelog * fix ci error * update * update
1 parent 07e6c7f commit 4592053

15 files changed

Lines changed: 333 additions & 270 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: internal
3+
packages:
4+
- "@azure-tools/typespec-python"
5+
---
6+
7+
sync eng change from upstream emitter

eng/scripts/sync_from_typespec.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@
3333
TYPESPEC_TEST_DIR = Path("packages/http-client-python/tests")
3434
AUTOREST_TEST_DIR = Path("packages/typespec-python/tests")
3535

36+
TYPESPEC_DEV_REQUIREMENTS = Path("packages/http-client-python/eng/scripts/ci/dev_requirements.txt")
37+
AUTOREST_DEV_REQUIREMENTS = Path("packages/typespec-python/dev_requirements.txt")
38+
39+
# Marker indicating where repo-specific content begins in dev_requirements.txt.
40+
# Everything from this line onward in the autorest file is preserved; everything
41+
# above is replaced with the upstream content (prefixed by a header comment).
42+
_DEV_REQUIREMENTS_HEADER = "# shall keep aligned with dev_requirements.txt of @typespec/http-client-python"
43+
_DEV_REQUIREMENTS_TAIL_MARKER = "# additional dependency needed for development"
44+
3645
# --- Marker patterns for requirements sync ---
3746
#
3847
# Convention in requirements files (e.g. azure.txt, unbranded.txt):
@@ -154,6 +163,46 @@ def sync_requirements(source_dir: Path, target_dir: Path) -> None:
154163
print(f" Copied: requirements/{filename}")
155164

156165

166+
# ---------------------------------------------------------------------------
167+
# dev_requirements.txt sync
168+
# ---------------------------------------------------------------------------
169+
170+
171+
def sync_dev_requirements(source_file: Path, target_file: Path) -> None:
172+
"""Sync upstream dev_requirements.txt, preserving repo-specific tail.
173+
174+
The target file layout is:
175+
<header>
176+
<upstream content>
177+
178+
# additional dependency needed for development
179+
<repo-specific deps> <-- preserved from existing target
180+
181+
Content from the tail marker onward in the existing target is kept;
182+
everything above is replaced with header + upstream.
183+
"""
184+
if not source_file.is_file():
185+
print(f" WARNING: {source_file} not found, skipping")
186+
return
187+
188+
upstream = source_file.read_text(encoding="utf-8").strip()
189+
190+
tail = ""
191+
if target_file.is_file():
192+
existing = target_file.read_text(encoding="utf-8")
193+
idx = existing.find(_DEV_REQUIREMENTS_TAIL_MARKER)
194+
if idx >= 0:
195+
tail = existing[idx:].strip()
196+
197+
content = f"{_DEV_REQUIREMENTS_HEADER}\n{upstream}\n"
198+
if tail:
199+
content += f"\n{tail}\n"
200+
201+
target_file.parent.mkdir(parents=True, exist_ok=True)
202+
target_file.write_text(content, encoding="utf-8", newline="\n")
203+
print(f" Synced: {target_file.name}")
204+
205+
157206
# ---------------------------------------------------------------------------
158207
# Test file sync
159208
# ---------------------------------------------------------------------------
@@ -246,14 +295,21 @@ def main() -> int:
246295
autorest_repo / AUTOREST_TEST_DIR / "requirements",
247296
)
248297

249-
# 3. Sync test files
298+
# 3. Sync dev_requirements.txt
299+
print("Syncing dev_requirements.txt...")
300+
sync_dev_requirements(
301+
typespec_repo / TYPESPEC_DEV_REQUIREMENTS,
302+
autorest_repo / AUTOREST_DEV_REQUIREMENTS,
303+
)
304+
305+
# 4. Sync test files
250306
print("Syncing test files...")
251307
sync_test_files(
252308
typespec_repo / TYPESPEC_TEST_DIR,
253309
autorest_repo / AUTOREST_TEST_DIR,
254310
)
255311

256-
# 4. Format TypeScript files
312+
# 5. Format TypeScript files
257313
ts_python_dir = autorest_repo / "packages" / "typespec-python"
258314
print("Running pnpm format...")
259315
result = subprocess.run(

packages/typespec-python/dev_requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# shall keep aligned with dev_requirements.txt of @typspec/http-client-python
1+
# shall keep aligned with dev_requirements.txt of @typespec/http-client-python
22
pyright==1.1.391
33
pylint==3.2.7
44
tox==4.23.2

packages/typespec-python/eng/scripts/ci/run_apiview.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,38 @@
1010

1111
import os
1212
import sys
13-
from subprocess import check_call, CalledProcessError
13+
from subprocess import run, TimeoutExpired
1414
import logging
1515
from util import run_check
1616

1717
logging.getLogger().setLevel(logging.INFO)
1818

19+
# Timeout for each apiview generation (seconds)
20+
APIVIEW_TIMEOUT = 30
21+
1922

2023
def _single_dir_apiview(mod):
21-
loop = 0
22-
while True:
24+
for attempt in range(2):
2325
try:
24-
check_call(
25-
[
26-
"apistubgen",
27-
"--pkg-path",
28-
str(mod.absolute()),
29-
]
26+
result = run(
27+
["apistubgen", "--pkg-path", str(mod.absolute())],
28+
capture_output=True,
29+
timeout=APIVIEW_TIMEOUT,
3030
)
31-
except CalledProcessError as e:
32-
if loop >= 2: # retry for maximum 3 times because sometimes the apistubgen has transient failure.
33-
logging.error("{} exited with apiview generation error {}".format(mod.stem, e.returncode))
31+
if result.returncode == 0:
32+
return True
33+
if attempt == 1:
34+
logging.error(f"{mod.stem} failed: {result.stderr.decode()[:200]}")
35+
return False
36+
except TimeoutExpired:
37+
if attempt == 1:
38+
logging.error(f"{mod.stem} timed out after {APIVIEW_TIMEOUT}s")
39+
return False
40+
except Exception as e:
41+
if attempt == 1:
42+
logging.error(f"{mod.stem} error: {e}")
3443
return False
35-
else:
36-
loop += 1
37-
continue
38-
return True
44+
return False
3945

4046

4147
if __name__ == "__main__":

packages/typespec-python/eng/scripts/ci/run_mypy.py

Lines changed: 31 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import os
1313
import logging
1414
import sys
15-
from util import run_check
15+
from util import run_check, get_package_namespace_dir
1616

1717
logging.getLogger().setLevel(logging.INFO)
1818

@@ -26,47 +26,37 @@ def get_config_file_location():
2626
return os.path.join(os.path.dirname(__file__), "config/mypy.ini")
2727

2828

29-
def _has_python_files(directory):
30-
"""Check if a directory contains any .py files recursively."""
31-
return any(directory.rglob("*.py"))
32-
33-
34-
def _single_dir_mypy(mod):
35-
try:
36-
inner_class = next(
37-
(
38-
d
39-
for d in mod.iterdir()
40-
if d.is_dir()
41-
and d.name not in ("build", "generated_tests", "specs", "generated_samples")
42-
and not str(d).endswith("egg-info")
43-
and _has_python_files(d)
44-
),
45-
None,
46-
)
47-
if inner_class is None:
48-
logging.warning("No valid source directory found in %s, skipping", mod)
49-
return True
50-
check_call(
51-
[
52-
sys.executable,
53-
"-m",
54-
"mypy",
55-
"--config-file",
56-
get_config_file_location(),
57-
"--ignore-missing",
58-
"--exclude",
59-
"build",
60-
str(inner_class.absolute()),
61-
]
62-
)
29+
def _single_dir_mypy(mod, retries=2):
30+
inner_class = get_package_namespace_dir(mod)
31+
if not inner_class:
32+
logging.info(f"No package directory found in {mod}, skipping")
6333
return True
64-
except CalledProcessError as e:
65-
logging.error("{} exited with mypy error {}".format(mod.stem, e.returncode))
66-
return False
67-
except Exception as e:
68-
logging.error("Unexpected error processing %s: %s", mod, e)
69-
return False
34+
for attempt in range(1, retries + 2):
35+
try:
36+
check_call(
37+
[
38+
sys.executable,
39+
"-m",
40+
"mypy",
41+
"--config-file",
42+
get_config_file_location(),
43+
"--ignore-missing",
44+
str(inner_class.absolute()),
45+
]
46+
)
47+
return True
48+
except CalledProcessError as e:
49+
if attempt <= retries:
50+
logging.warning(
51+
"{} mypy attempt {} failed (exit {}), retrying...".format(inner_class.stem, attempt, e.returncode)
52+
)
53+
else:
54+
logging.error("{} exited with mypy error {}".format(inner_class.stem, e.returncode))
55+
return False
56+
except Exception as e:
57+
logging.error("Unexpected error processing %s: %s", mod, e)
58+
return False
59+
return False
7060

7161

7262
if __name__ == "__main__":

packages/typespec-python/eng/scripts/ci/run_pylint.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import os
1313
import logging
1414
import sys
15-
from util import run_check
15+
from util import run_check, get_package_namespace_dir
1616

1717
logging.getLogger().setLevel(logging.INFO)
1818

@@ -26,27 +26,12 @@ def get_rfc_file_location():
2626
return os.path.join(os.path.dirname(__file__), "config/pylintrc")
2727

2828

29-
def _has_python_files(directory):
30-
"""Check if a directory contains any .py files recursively."""
31-
return any(directory.rglob("*.py"))
32-
33-
3429
def _single_dir_pylint(mod):
30+
inner_class = get_package_namespace_dir(mod)
31+
if not inner_class:
32+
logging.info(f"No package directory found in {mod}, skipping")
33+
return True
3534
try:
36-
inner_class = next(
37-
(
38-
d
39-
for d in mod.iterdir()
40-
if d.is_dir()
41-
and d.name not in ("build", "generated_tests", "specs", "generated_samples")
42-
and not str(d).endswith("egg-info")
43-
and _has_python_files(d)
44-
),
45-
None,
46-
)
47-
if inner_class is None:
48-
logging.warning("No valid source directory found in %s, skipping", mod)
49-
return True
5035
check_call(
5136
[
5237
sys.executable,

packages/typespec-python/eng/scripts/ci/run_pyright.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import logging
1414
import sys
1515
import time
16-
from util import run_check
16+
from util import run_check, get_package_namespace_dir
1717

1818
logging.getLogger().setLevel(logging.INFO)
1919

@@ -27,27 +27,12 @@ def get_pyright_config_file_location():
2727
return os.path.join(os.path.dirname(__file__), "config/pyrightconfig.json")
2828

2929

30-
def _has_python_files(directory):
31-
"""Check if a directory contains any .py files recursively."""
32-
return any(directory.rglob("*.py"))
33-
34-
3530
def _single_dir_pyright(mod):
31+
inner_class = get_package_namespace_dir(mod)
32+
if not inner_class:
33+
logging.info(f"No package directory found in {mod}, skipping")
34+
return True
3635
try:
37-
inner_class = next(
38-
(
39-
d
40-
for d in mod.iterdir()
41-
if d.is_dir()
42-
and d.name not in ("build", "generated_tests", "specs", "generated_samples")
43-
and not str(d).endswith("egg-info")
44-
and _has_python_files(d)
45-
),
46-
None,
47-
)
48-
if inner_class is None:
49-
logging.warning("No valid source directory found in %s, skipping", mod)
50-
return True
5136
retries = 3
5237
while retries:
5338
try:

packages/typespec-python/eng/scripts/ci/run_sphinx_build.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,21 @@
88
# This script is used to execute sphinx documentation build within a tox environment.
99
# It uses a central sphinx configuration and validates docstrings by running sphinx-build.
1010

11-
from subprocess import check_call, CalledProcessError
11+
from subprocess import run, TimeoutExpired
1212
import os
1313
import logging
1414
import sys
1515
from pathlib import Path
16-
from util import run_check
16+
from util import run_check, SKIP_PACKAGE_DIRS
1717

1818
logging.getLogger().setLevel(logging.INFO)
1919

2020
# Get the central Sphinx config directory
2121
SPHINX_CONF_DIR = os.path.abspath(os.path.dirname(__file__))
2222

23+
# Timeout for each sphinx build (seconds)
24+
SPHINX_TIMEOUT = 120
25+
2326

2427
def _create_minimal_index_rst(docs_dir, package_name, module_names):
2528
"""Create a minimal index.rst file for sphinx to process."""
@@ -50,7 +53,12 @@ def _single_dir_sphinx(mod):
5053

5154
# Find the actual Python package directories
5255
package_dirs = [
53-
d for d in mod.iterdir() if d.is_dir() and not d.name.startswith("_") and (d / "__init__.py").exists()
56+
d
57+
for d in mod.iterdir()
58+
if d.is_dir()
59+
and not d.name.startswith("_")
60+
and d.name not in SKIP_PACKAGE_DIRS
61+
and (d / "__init__.py").exists()
5462
]
5563

5664
if not package_dirs:
@@ -85,7 +93,7 @@ def _single_dir_sphinx(mod):
8593
sys.path.insert(0, str(mod.absolute()))
8694

8795
try:
88-
result = check_call(
96+
result = run(
8997
[
9098
sys.executable,
9199
"-m",
@@ -100,12 +108,19 @@ def _single_dir_sphinx(mod):
100108
"-q", # Quiet mode (only show warnings/errors)
101109
str(docs_dir.absolute()), # Source directory
102110
str(output_dir.absolute()), # Output directory
103-
]
111+
],
112+
capture_output=True,
113+
timeout=SPHINX_TIMEOUT,
104114
)
105-
logging.info(f"Sphinx build completed successfully for {mod.stem}")
106-
return True
107-
except CalledProcessError as e:
108-
logging.error(f"{mod.stem} exited with sphinx build error {e.returncode}")
115+
if result.returncode == 0:
116+
return True
117+
logging.error(f"{mod.stem} sphinx error: {result.stderr.decode()[:500]}")
118+
return False
119+
except TimeoutExpired:
120+
logging.error(f"{mod.stem} timed out after {SPHINX_TIMEOUT}s")
121+
return False
122+
except Exception as e:
123+
logging.error(f"{mod.stem} sphinx error: {e}")
109124
return False
110125
finally:
111126
# Remove from sys.path

0 commit comments

Comments
 (0)