@typespec/http-client-python 0.28.3 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/emitter/code-model.d.ts.map +1 -1
- package/dist/emitter/code-model.js +1 -0
- package/dist/emitter/code-model.js.map +1 -1
- package/dist/emitter/constants.d.ts +6 -0
- package/dist/emitter/constants.d.ts.map +1 -0
- package/dist/emitter/constants.js +27 -0
- package/dist/emitter/constants.js.map +1 -0
- package/dist/emitter/emitter.d.ts.map +1 -1
- package/dist/emitter/emitter.js +169 -94
- package/dist/emitter/emitter.js.map +1 -1
- package/dist/emitter/http.d.ts.map +1 -1
- package/dist/emitter/http.js +33 -1
- package/dist/emitter/http.js.map +1 -1
- package/dist/emitter/lib.d.ts +33 -2
- package/dist/emitter/lib.d.ts.map +1 -1
- package/dist/emitter/lib.js +17 -0
- package/dist/emitter/lib.js.map +1 -1
- package/dist/emitter/run-python3.js +1 -1
- package/dist/emitter/run-python3.js.map +1 -1
- package/dist/emitter/types.d.ts.map +1 -1
- package/dist/emitter/types.js +31 -5
- package/dist/emitter/types.js.map +1 -1
- package/emitter/src/code-model.ts +1 -0
- package/emitter/src/constants.ts +27 -0
- package/emitter/src/emitter.ts +216 -96
- package/emitter/src/http.ts +29 -1
- package/emitter/src/lib.ts +20 -0
- package/emitter/src/run-python3.ts +1 -1
- package/emitter/src/types.ts +35 -5
- package/emitter/temp/tsconfig.tsbuildinfo +1 -1
- package/emitter/test/utils.test.ts +3 -0
- package/emitter/tsconfig.build.json +2 -1
- package/emitter/tsconfig.json +2 -1
- package/eng/scripts/ci/config/eslint-ci.config.mjs +10 -0
- package/eng/scripts/ci/config/mypy.ini +1 -1
- package/eng/scripts/ci/config/pylintrc +1 -1
- package/eng/scripts/ci/config/pyrightconfig.json +1 -1
- package/eng/scripts/ci/dev_requirements.txt +1 -1
- package/eng/scripts/ci/lint.ts +11 -4
- package/eng/scripts/ci/regenerate-common.ts +407 -136
- package/eng/scripts/ci/regenerate.ts +103 -194
- package/eng/scripts/ci/run-tests.ts +153 -48
- package/eng/scripts/ci/run_apiview.py +22 -16
- package/eng/scripts/ci/run_mypy.py +28 -18
- package/eng/scripts/ci/run_pylint.py +23 -16
- package/eng/scripts/ci/run_pyright.py +5 -2
- package/eng/scripts/ci/run_sphinx_build.py +24 -9
- package/eng/scripts/ci/util.py +61 -25
- package/eng/scripts/setup/__pycache__/package_manager.cpython-311.pyc +0 -0
- package/eng/scripts/setup/__pycache__/venvtools.cpython-311.pyc +0 -0
- package/eng/scripts/setup/build_pygen_wheel.py +3 -3
- package/eng/scripts/setup/install.py +3 -3
- package/eng/scripts/setup/install.ts +1 -1
- package/eng/scripts/setup/prepare.py +2 -2
- package/eng/scripts/setup/run-python3.ts +1 -1
- package/eng/scripts/setup/run_batch.py +197 -0
- package/eng/scripts/setup/venvtools.py +0 -1
- package/generator/build/lib/pygen/__init__.py +1 -3
- package/generator/build/lib/pygen/codegen/__init__.py +0 -1
- package/generator/build/lib/pygen/codegen/models/base.py +0 -1
- package/generator/build/lib/pygen/codegen/models/client.py +2 -1
- package/generator/build/lib/pygen/codegen/models/code_model.py +1 -0
- package/generator/build/lib/pygen/codegen/models/enum_type.py +0 -1
- package/generator/build/lib/pygen/codegen/models/model_type.py +1 -7
- package/generator/build/lib/pygen/codegen/models/parameter.py +16 -14
- package/generator/build/lib/pygen/codegen/serializers/__init__.py +27 -12
- package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +12 -3
- package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +6 -2
- package/generator/build/lib/pygen/codegen/serializers/import_serializer.py +17 -4
- package/generator/build/lib/pygen/codegen/serializers/parameter_serializer.py +17 -11
- package/generator/build/lib/pygen/codegen/templates/model_base.py.jinja2 +19 -7
- package/generator/build/lib/pygen/codegen/templates/packaging_templates/README.md.jinja2 +11 -3
- package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +1 -1
- package/generator/build/lib/pygen/codegen/templates/serialization.py.jinja2 +5 -1
- package/generator/build/lib/pygen/preprocess/__init__.py +37 -30
- package/generator/component-detection-pip-report.json +7 -7
- package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
- package/generator/pygen/__init__.py +1 -3
- package/generator/pygen/codegen/__init__.py +0 -1
- package/generator/pygen/codegen/models/base.py +0 -1
- package/generator/pygen/codegen/models/client.py +2 -1
- package/generator/pygen/codegen/models/code_model.py +1 -0
- package/generator/pygen/codegen/models/enum_type.py +0 -1
- package/generator/pygen/codegen/models/model_type.py +1 -7
- package/generator/pygen/codegen/models/parameter.py +16 -14
- package/generator/pygen/codegen/serializers/__init__.py +27 -12
- package/generator/pygen/codegen/serializers/builder_serializer.py +12 -3
- package/generator/pygen/codegen/serializers/general_serializer.py +6 -2
- package/generator/pygen/codegen/serializers/import_serializer.py +17 -4
- package/generator/pygen/codegen/serializers/parameter_serializer.py +17 -11
- package/generator/pygen/codegen/templates/model_base.py.jinja2 +19 -7
- package/generator/pygen/codegen/templates/packaging_templates/README.md.jinja2 +11 -3
- package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +1 -1
- package/generator/pygen/codegen/templates/serialization.py.jinja2 +5 -1
- package/generator/pygen/preprocess/__init__.py +37 -30
- package/generator/pygen.egg-info/PKG-INFO +1 -2
- package/generator/pygen.egg-info/requires.txt +1 -1
- package/generator/setup.py +1 -2
- package/package.json +24 -9
|
@@ -10,32 +10,38 @@
|
|
|
10
10
|
|
|
11
11
|
import os
|
|
12
12
|
import sys
|
|
13
|
-
from subprocess import
|
|
13
|
+
from subprocess import run, TimeoutExpired
|
|
14
14
|
import logging
|
|
15
15
|
from util import run_check
|
|
16
16
|
|
|
17
17
|
logging.getLogger().setLevel(logging.INFO)
|
|
18
18
|
|
|
19
|
+
# Timeout for each apiview generation (seconds)
|
|
20
|
+
APIVIEW_TIMEOUT = 30
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
def _single_dir_apiview(mod):
|
|
21
|
-
|
|
22
|
-
while True:
|
|
24
|
+
for attempt in range(2):
|
|
23
25
|
try:
|
|
24
|
-
|
|
25
|
-
[
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
str(mod.absolute()),
|
|
29
|
-
]
|
|
26
|
+
result = run(
|
|
27
|
+
["apistubgen", "--pkg-path", str(mod.absolute())],
|
|
28
|
+
capture_output=True,
|
|
29
|
+
timeout=APIVIEW_TIMEOUT,
|
|
30
30
|
)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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}")
|
|
34
43
|
return False
|
|
35
|
-
|
|
36
|
-
loop += 1
|
|
37
|
-
continue
|
|
38
|
-
return True
|
|
44
|
+
return False
|
|
39
45
|
|
|
40
46
|
|
|
41
47
|
if __name__ == "__main__":
|
|
@@ -12,7 +12,7 @@ from subprocess import check_call, CalledProcessError
|
|
|
12
12
|
import os
|
|
13
13
|
import logging
|
|
14
14
|
import sys
|
|
15
|
-
from util import run_check
|
|
15
|
+
from util import run_check, get_package_namespace_dir
|
|
16
16
|
|
|
17
17
|
logging.getLogger().setLevel(logging.INFO)
|
|
18
18
|
|
|
@@ -26,24 +26,34 @@ def get_config_file_location():
|
|
|
26
26
|
return os.path.join(os.path.dirname(__file__), "config/mypy.ini")
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def _single_dir_mypy(mod):
|
|
30
|
-
inner_class =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
[
|
|
34
|
-
sys.executable,
|
|
35
|
-
"-m",
|
|
36
|
-
"mypy",
|
|
37
|
-
"--config-file",
|
|
38
|
-
get_config_file_location(),
|
|
39
|
-
"--ignore-missing",
|
|
40
|
-
str(inner_class.absolute()),
|
|
41
|
-
]
|
|
42
|
-
)
|
|
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")
|
|
43
33
|
return True
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
return False
|
|
47
57
|
|
|
48
58
|
|
|
49
59
|
if __name__ == "__main__":
|
|
@@ -12,7 +12,7 @@ from subprocess import check_call, CalledProcessError
|
|
|
12
12
|
import os
|
|
13
13
|
import logging
|
|
14
14
|
import sys
|
|
15
|
-
from util import run_check
|
|
15
|
+
from util import run_check, get_package_namespace_dir
|
|
16
16
|
|
|
17
17
|
logging.getLogger().setLevel(logging.INFO)
|
|
18
18
|
|
|
@@ -27,22 +27,29 @@ def get_rfc_file_location():
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _single_dir_pylint(mod):
|
|
30
|
-
inner_class =
|
|
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
|
|
34
|
+
# Only load the Azure pylint guidelines checker plugin for azure packages.
|
|
35
|
+
# The plugin (azure-pylint-guidelines-checker) is only installed in the
|
|
36
|
+
# lint-azure tox environment and is not available for unbranded packages.
|
|
37
|
+
is_azure = "azure" in mod.parts
|
|
38
|
+
pylint_args = [
|
|
39
|
+
sys.executable,
|
|
40
|
+
"-m",
|
|
41
|
+
"pylint",
|
|
42
|
+
"--rcfile={}".format(get_rfc_file_location()),
|
|
43
|
+
"--evaluation=(max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention + info)/ statement) * 10)))",
|
|
44
|
+
"--output-format=parseable",
|
|
45
|
+
"--recursive=y",
|
|
46
|
+
"--py-version=3.10",
|
|
47
|
+
]
|
|
48
|
+
if is_azure:
|
|
49
|
+
pylint_args.append("--load-plugins=pylint_guidelines_checker")
|
|
50
|
+
pylint_args.append(str(inner_class.absolute()))
|
|
31
51
|
try:
|
|
32
|
-
check_call(
|
|
33
|
-
[
|
|
34
|
-
sys.executable,
|
|
35
|
-
"-m",
|
|
36
|
-
"pylint",
|
|
37
|
-
"--rcfile={}".format(get_rfc_file_location()),
|
|
38
|
-
"--evaluation=(max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention + info)/ statement) * 10)))",
|
|
39
|
-
"--load-plugins=pylint_guidelines_checker",
|
|
40
|
-
"--output-format=parseable",
|
|
41
|
-
"--recursive=y",
|
|
42
|
-
"--py-version=3.9",
|
|
43
|
-
str(inner_class.absolute()),
|
|
44
|
-
]
|
|
45
|
-
)
|
|
52
|
+
check_call(pylint_args)
|
|
46
53
|
return True
|
|
47
54
|
except CalledProcessError as e:
|
|
48
55
|
logging.error("{} exited with linting error {}".format(str(inner_class.absolute()), e.returncode))
|
|
@@ -13,7 +13,7 @@ from subprocess import check_output, CalledProcessError
|
|
|
13
13
|
import logging
|
|
14
14
|
import sys
|
|
15
15
|
import time
|
|
16
|
-
from util import run_check
|
|
16
|
+
from util import run_check, get_package_namespace_dir
|
|
17
17
|
|
|
18
18
|
logging.getLogger().setLevel(logging.INFO)
|
|
19
19
|
|
|
@@ -28,7 +28,10 @@ def get_pyright_config_file_location():
|
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def _single_dir_pyright(mod):
|
|
31
|
-
inner_class =
|
|
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
|
|
32
35
|
retries = 3
|
|
33
36
|
while retries:
|
|
34
37
|
try:
|
|
@@ -8,18 +8,21 @@
|
|
|
8
8
|
# This script is used to execute sphinx documentation build within a tox environment.
|
|
9
9
|
# It uses a central sphinx configuration and validates docstrings by running sphinx-build.
|
|
10
10
|
|
|
11
|
-
from subprocess import
|
|
11
|
+
from subprocess import run, TimeoutExpired
|
|
12
12
|
import os
|
|
13
13
|
import logging
|
|
14
14
|
import sys
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from util import run_check
|
|
16
|
+
from util import run_check, SKIP_PACKAGE_DIRS
|
|
17
17
|
|
|
18
18
|
logging.getLogger().setLevel(logging.INFO)
|
|
19
19
|
|
|
20
20
|
# Get the central Sphinx config directory
|
|
21
21
|
SPHINX_CONF_DIR = os.path.abspath(os.path.dirname(__file__))
|
|
22
22
|
|
|
23
|
+
# Timeout for each sphinx build (seconds)
|
|
24
|
+
SPHINX_TIMEOUT = 120
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
def _create_minimal_index_rst(docs_dir, package_name, module_names):
|
|
25
28
|
"""Create a minimal index.rst file for sphinx to process."""
|
|
@@ -50,7 +53,12 @@ def _single_dir_sphinx(mod):
|
|
|
50
53
|
|
|
51
54
|
# Find the actual Python package directories
|
|
52
55
|
package_dirs = [
|
|
53
|
-
d
|
|
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()
|
|
54
62
|
]
|
|
55
63
|
|
|
56
64
|
if not package_dirs:
|
|
@@ -85,7 +93,7 @@ def _single_dir_sphinx(mod):
|
|
|
85
93
|
sys.path.insert(0, str(mod.absolute()))
|
|
86
94
|
|
|
87
95
|
try:
|
|
88
|
-
result =
|
|
96
|
+
result = run(
|
|
89
97
|
[
|
|
90
98
|
sys.executable,
|
|
91
99
|
"-m",
|
|
@@ -100,12 +108,19 @@ def _single_dir_sphinx(mod):
|
|
|
100
108
|
"-q", # Quiet mode (only show warnings/errors)
|
|
101
109
|
str(docs_dir.absolute()), # Source directory
|
|
102
110
|
str(output_dir.absolute()), # Output directory
|
|
103
|
-
]
|
|
111
|
+
],
|
|
112
|
+
capture_output=True,
|
|
113
|
+
timeout=SPHINX_TIMEOUT,
|
|
104
114
|
)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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}")
|
|
109
124
|
return False
|
|
110
125
|
finally:
|
|
111
126
|
# Remove from sys.path
|
package/eng/scripts/ci/util.py
CHANGED
|
@@ -8,14 +8,32 @@ import os
|
|
|
8
8
|
import logging
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import argparse
|
|
11
|
-
from
|
|
11
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
12
12
|
|
|
13
13
|
logging.getLogger().setLevel(logging.INFO)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
# Root is the tests directory (4 levels up from this file: ci -> scripts -> eng -> package_root, then into tests)
|
|
16
|
+
ROOT_FOLDER = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..", "..", "..", "tests"))
|
|
16
17
|
|
|
17
18
|
IGNORE_FOLDER = []
|
|
18
19
|
|
|
20
|
+
# Directories inside each generated package that should be skipped by all CI checks.
|
|
21
|
+
# These are auto-generated test/sample scaffolding, not the actual SDK code.
|
|
22
|
+
SKIP_PACKAGE_DIRS = {"generated_tests", "generated_samples", "build", "__pycache__", ".pytest_cache"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_package_namespace_dir(mod):
|
|
26
|
+
"""Find the actual namespace directory inside a generated package, skipping non-SDK dirs."""
|
|
27
|
+
for d in mod.iterdir():
|
|
28
|
+
if (
|
|
29
|
+
d.is_dir()
|
|
30
|
+
and not d.name.startswith("_")
|
|
31
|
+
and not d.name.endswith("egg-info")
|
|
32
|
+
and d.name not in SKIP_PACKAGE_DIRS
|
|
33
|
+
):
|
|
34
|
+
return d
|
|
35
|
+
return None
|
|
36
|
+
|
|
19
37
|
|
|
20
38
|
def run_check(name, call_back, log_info):
|
|
21
39
|
parser = argparse.ArgumentParser(
|
|
@@ -25,16 +43,9 @@ def run_check(name, call_back, log_info):
|
|
|
25
43
|
"-t",
|
|
26
44
|
"--test-folder",
|
|
27
45
|
dest="test_folder",
|
|
28
|
-
help="The test folder we're in. Can be 'azure' or '
|
|
46
|
+
help="The test folder we're in. Can be 'azure' or 'unbranded'",
|
|
29
47
|
required=True,
|
|
30
48
|
)
|
|
31
|
-
parser.add_argument(
|
|
32
|
-
"-g",
|
|
33
|
-
"--generator",
|
|
34
|
-
dest="generator",
|
|
35
|
-
help="The generator we're using. Can be 'legacy', 'version-tolerant'.",
|
|
36
|
-
required=False,
|
|
37
|
-
)
|
|
38
49
|
parser.add_argument(
|
|
39
50
|
"-f",
|
|
40
51
|
"--file-name",
|
|
@@ -46,27 +57,52 @@ def run_check(name, call_back, log_info):
|
|
|
46
57
|
"-s",
|
|
47
58
|
"--subfolder",
|
|
48
59
|
dest="subfolder",
|
|
49
|
-
help="The
|
|
60
|
+
help="The subfolder containing generated code, default to 'generated'.",
|
|
50
61
|
required=False,
|
|
51
|
-
default="
|
|
62
|
+
default="generated",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"-j",
|
|
66
|
+
"--jobs",
|
|
67
|
+
dest="jobs",
|
|
68
|
+
help="Number of parallel jobs (default: CPU count)",
|
|
69
|
+
type=int,
|
|
70
|
+
required=False,
|
|
71
|
+
default=max(1, os.cpu_count()),
|
|
52
72
|
)
|
|
53
73
|
|
|
54
74
|
args = parser.parse_args()
|
|
55
75
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
pkg_dir /= Path(args.generator)
|
|
59
|
-
if args.subfolder:
|
|
60
|
-
pkg_dir /= Path(args.subfolder)
|
|
76
|
+
# Path structure: tests/generated/{test_folder}/
|
|
77
|
+
pkg_dir = Path(ROOT_FOLDER) / Path(args.subfolder) / Path(args.test_folder)
|
|
61
78
|
dirs = [d for d in pkg_dir.iterdir() if d.is_dir() and not d.stem.startswith("_") and d.stem not in IGNORE_FOLDER]
|
|
62
79
|
if args.file_name:
|
|
63
80
|
dirs = [d for d in dirs if args.file_name.lower() in d.stem.lower()]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
|
|
82
|
+
if not dirs:
|
|
83
|
+
logging.info("No directories to process")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
logging.info(f"Processing {len(dirs)} packages with {args.jobs} parallel jobs...")
|
|
87
|
+
|
|
88
|
+
failed = []
|
|
89
|
+
succeeded = 0
|
|
90
|
+
|
|
91
|
+
with ProcessPoolExecutor(max_workers=args.jobs) as executor:
|
|
92
|
+
futures = {executor.submit(call_back, d): d for d in dirs}
|
|
93
|
+
for future in as_completed(futures):
|
|
94
|
+
pkg = futures[future]
|
|
95
|
+
try:
|
|
96
|
+
if future.result():
|
|
97
|
+
succeeded += 1
|
|
98
|
+
else:
|
|
99
|
+
failed.append(pkg.stem)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logging.error(f"{pkg.stem} raised exception: {e}")
|
|
102
|
+
failed.append(pkg.stem)
|
|
103
|
+
|
|
104
|
+
logging.info(f"{log_info}: {succeeded} succeeded, {len(failed)} failed")
|
|
105
|
+
|
|
106
|
+
if failed:
|
|
107
|
+
logging.error(f"{log_info} failed for: {', '.join(failed)}")
|
|
72
108
|
exit(1)
|
|
Binary file
|
|
Binary file
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
# --------------------------------------------------------------------------
|
|
8
8
|
import sys
|
|
9
9
|
|
|
10
|
-
if not sys.version_info >= (3,
|
|
11
|
-
raise Exception("Autorest for Python extension requires Python 3.
|
|
10
|
+
if not sys.version_info >= (3, 10, 0):
|
|
11
|
+
raise Exception("Autorest for Python extension requires Python 3.10 at least")
|
|
12
12
|
|
|
13
13
|
try:
|
|
14
14
|
from package_manager import detect_package_manager, PackageManagerNotFoundError
|
|
@@ -18,7 +18,7 @@ except (ImportError, ModuleNotFoundError, PackageManagerNotFoundError):
|
|
|
18
18
|
raise Exception("Your Python installation doesn't have a suitable package manager (pip or uv) available")
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
# Now we have a package manager (pip or uv) and Py >= 3.
|
|
21
|
+
# Now we have a package manager (pip or uv) and Py >= 3.10, go to work
|
|
22
22
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
# --------------------------------------------------------------------------
|
|
8
8
|
import sys
|
|
9
9
|
|
|
10
|
-
if not sys.version_info >= (3,
|
|
10
|
+
if not sys.version_info >= (3, 10, 0):
|
|
11
11
|
print(
|
|
12
|
-
"Warning: Autorest for Python extension requires Python 3.
|
|
12
|
+
"Warning: Autorest for Python extension requires Python 3.10 at least. We will run your code with Pyodide since your Python version isn't adequate."
|
|
13
13
|
)
|
|
14
14
|
sys.exit(2) # Exit code 2 for inadequate environment
|
|
15
15
|
|
|
@@ -32,7 +32,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
32
32
|
sys.exit(2) # Exit code 2 for inadequate environment
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
# Now we have a package manager (uv or pip) and Py >= 3.
|
|
35
|
+
# Now we have a package manager (uv or pip) and Py >= 3.10, go to work
|
|
36
36
|
# At this point, both Python and package manager are confirmed to be available
|
|
37
37
|
# Any failures from here should fail the npm install, not fallback to Pyodide
|
|
38
38
|
|
|
@@ -7,7 +7,7 @@ async function main() {
|
|
|
7
7
|
try {
|
|
8
8
|
// First try to resolve Python path
|
|
9
9
|
pythonCommand = await patchPythonPath(["python", "./eng/scripts/setup/install.py"], {
|
|
10
|
-
version: ">=3.
|
|
10
|
+
version: ">=3.10",
|
|
11
11
|
environmentVariable: "AUTOREST_PYTHON_EXE",
|
|
12
12
|
});
|
|
13
13
|
} catch (error) {
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
# --------------------------------------------------------------------------
|
|
8
8
|
import sys
|
|
9
9
|
|
|
10
|
-
if not sys.version_info >= (3,
|
|
10
|
+
if not sys.version_info >= (3, 10, 0):
|
|
11
11
|
raise Warning(
|
|
12
|
-
"Autorest for Python extension requires Python 3.
|
|
12
|
+
"Autorest for Python extension requires Python 3.10 at least. We will run your code with Pyodide since your Python version isn't adequate."
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
from pathlib import Path
|
|
@@ -11,7 +11,7 @@ import { patchPythonPath } from "./system-requirements.js";
|
|
|
11
11
|
|
|
12
12
|
export async function runPython3(...args: string[]) {
|
|
13
13
|
const command = await patchPythonPath(["python", ...args], {
|
|
14
|
-
version: ">=3.
|
|
14
|
+
version: ">=3.10",
|
|
15
15
|
environmentVariable: "AUTOREST_PYTHON_EXE",
|
|
16
16
|
});
|
|
17
17
|
cp.execSync(command.join(" "), {
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
"""
|
|
7
|
+
Batch process multiple TypeSpec YAML files in a single Python process.
|
|
8
|
+
This avoids the overhead of spawning a new Python process for each spec.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
17
|
+
from multiprocessing import freeze_support
|
|
18
|
+
|
|
19
|
+
# Add the generator to the path
|
|
20
|
+
_ROOT_DIR = Path(__file__).parent.parent.parent.parent
|
|
21
|
+
sys.path.insert(0, str(_ROOT_DIR / "generator"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def process_single_spec(config_path_str: str) -> tuple[str, bool, str]:
|
|
25
|
+
"""Process a single spec from its config file.
|
|
26
|
+
|
|
27
|
+
Returns: (output_dir, success, error_message)
|
|
28
|
+
"""
|
|
29
|
+
# Import inside function for multiprocessing compatibility
|
|
30
|
+
from pygen import preprocess, codegen
|
|
31
|
+
|
|
32
|
+
config_path = Path(config_path_str)
|
|
33
|
+
try:
|
|
34
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
35
|
+
config = json.load(f)
|
|
36
|
+
|
|
37
|
+
yaml_path = config["yamlPath"]
|
|
38
|
+
command_args = config["commandArgs"]
|
|
39
|
+
output_dir = config["outputDir"]
|
|
40
|
+
|
|
41
|
+
# Pass command args directly to pygen - pygen expects hyphenated keys
|
|
42
|
+
# Remove keys that shouldn't be passed to pygen.
|
|
43
|
+
# Also coerce the string values "true"/"false" to real booleans, matching the behavior
|
|
44
|
+
# of pygen.utils.parse_args (the CLI path). Without this, the emitter passes string
|
|
45
|
+
# "false" for flags like keep-setup-py, which is truthy in Python and causes pygen to
|
|
46
|
+
# take the wrong branch (e.g. generating setup.py instead of pyproject.toml).
|
|
47
|
+
def _coerce(value):
|
|
48
|
+
if value == "true":
|
|
49
|
+
return True
|
|
50
|
+
if value == "false":
|
|
51
|
+
return False
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
pygen_args = {k: _coerce(v) for k, v in command_args.items() if k not in ["emit-yaml-only"]}
|
|
55
|
+
|
|
56
|
+
# Run preprocess and codegen (black is batched at the end for performance)
|
|
57
|
+
preprocess.PreProcessPlugin(output_folder=output_dir, tsp_file=yaml_path, **pygen_args).process()
|
|
58
|
+
|
|
59
|
+
codegen.CodeGenerator(output_folder=output_dir, tsp_file=yaml_path, **pygen_args).process()
|
|
60
|
+
|
|
61
|
+
# Clean up the config file
|
|
62
|
+
config_path.unlink()
|
|
63
|
+
|
|
64
|
+
return (output_dir, True, "")
|
|
65
|
+
except Exception as e:
|
|
66
|
+
return (str(config_path), False, str(e))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def render_progress_bar(completed: int, failed: int, total: int, width: int = 40) -> str:
|
|
70
|
+
"""Render a progress bar with green for success and red for failures."""
|
|
71
|
+
success_count = completed - failed
|
|
72
|
+
success_width = round((success_count / total) * width) if total > 0 else 0
|
|
73
|
+
fail_width = round((failed / total) * width) if total > 0 else 0
|
|
74
|
+
empty_width = width - success_width - fail_width
|
|
75
|
+
|
|
76
|
+
# ANSI color codes
|
|
77
|
+
green_bg = "\033[42m"
|
|
78
|
+
red_bg = "\033[41m"
|
|
79
|
+
reset = "\033[0m"
|
|
80
|
+
dim = "\033[2m"
|
|
81
|
+
cyan = "\033[36m"
|
|
82
|
+
|
|
83
|
+
success_bar = f"{green_bg}{' ' * success_width}{reset}"
|
|
84
|
+
fail_bar = f"{red_bg}{' ' * fail_width}{reset}" if failed > 0 else ""
|
|
85
|
+
empty_bar = f"{dim}{'░' * max(0, empty_width)}{reset}"
|
|
86
|
+
|
|
87
|
+
percent = round((completed / total) * 100) if total > 0 else 0
|
|
88
|
+
return f"{success_bar}{fail_bar}{empty_bar} {cyan}{percent}%{reset} ({completed}/{total})"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def collect_config_files(generated_dir: str, flavor: str) -> list[str]:
|
|
92
|
+
"""Collect all .tsp-codegen-*.json config files from the generated directory."""
|
|
93
|
+
flavor_dir = Path(generated_dir) / "tests" / "generated" / flavor
|
|
94
|
+
if not flavor_dir.exists():
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
config_files = []
|
|
98
|
+
for pkg_dir in flavor_dir.iterdir():
|
|
99
|
+
if pkg_dir.is_dir():
|
|
100
|
+
for f in pkg_dir.iterdir():
|
|
101
|
+
if f.name.startswith(".tsp-codegen-") and f.name.endswith(".json"):
|
|
102
|
+
config_files.append(str(f))
|
|
103
|
+
return config_files
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main():
|
|
107
|
+
parser = argparse.ArgumentParser(description="Batch process TypeSpec YAML files")
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--generated-dir",
|
|
110
|
+
required=True,
|
|
111
|
+
help="Path to the generator directory (config files are in ../tests/generated/<flavor>/)",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--flavor",
|
|
115
|
+
required=True,
|
|
116
|
+
help="Flavor to process (azure or unbranded)",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--jobs",
|
|
120
|
+
type=int,
|
|
121
|
+
default=4,
|
|
122
|
+
help="Number of parallel jobs (default: 4)",
|
|
123
|
+
)
|
|
124
|
+
args = parser.parse_args()
|
|
125
|
+
|
|
126
|
+
# Discover config files from the generated directory
|
|
127
|
+
config_files = collect_config_files(args.generated_dir, args.flavor)
|
|
128
|
+
total = len(config_files)
|
|
129
|
+
|
|
130
|
+
if total == 0:
|
|
131
|
+
print("No config files found, nothing to process")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
print(f"Processing {total} specs with {args.jobs} parallel jobs...")
|
|
135
|
+
|
|
136
|
+
succeeded = 0
|
|
137
|
+
failed = 0
|
|
138
|
+
failed_specs = []
|
|
139
|
+
output_dirs = []
|
|
140
|
+
is_tty = sys.stdout.isatty()
|
|
141
|
+
|
|
142
|
+
def update_progress():
|
|
143
|
+
if is_tty:
|
|
144
|
+
sys.stdout.write(f"\r{render_progress_bar(succeeded + failed, failed, total)}")
|
|
145
|
+
sys.stdout.flush()
|
|
146
|
+
|
|
147
|
+
# Initial progress bar
|
|
148
|
+
update_progress()
|
|
149
|
+
|
|
150
|
+
# Use ProcessPoolExecutor for true parallelism (bypasses GIL)
|
|
151
|
+
with ProcessPoolExecutor(max_workers=args.jobs) as executor:
|
|
152
|
+
futures = {executor.submit(process_single_spec, cf): cf for cf in config_files}
|
|
153
|
+
|
|
154
|
+
for future in as_completed(futures):
|
|
155
|
+
output_dir, success, error = future.result()
|
|
156
|
+
if success:
|
|
157
|
+
succeeded += 1
|
|
158
|
+
output_dirs.append(output_dir)
|
|
159
|
+
else:
|
|
160
|
+
failed += 1
|
|
161
|
+
failed_specs.append(f"{output_dir}: {error}")
|
|
162
|
+
# Fail-fast: cancel pending futures on first failure
|
|
163
|
+
print(f"\n\033[31m[FAIL-FAST] Cancelling remaining tasks after failure...\033[0m")
|
|
164
|
+
for f in futures:
|
|
165
|
+
f.cancel()
|
|
166
|
+
break
|
|
167
|
+
update_progress()
|
|
168
|
+
|
|
169
|
+
# Clear progress bar line
|
|
170
|
+
if is_tty:
|
|
171
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
172
|
+
sys.stdout.flush()
|
|
173
|
+
|
|
174
|
+
# Print failures at the end
|
|
175
|
+
if failed_specs:
|
|
176
|
+
print("\n\033[31mFailed specs:\033[0m")
|
|
177
|
+
for spec in failed_specs:
|
|
178
|
+
print(f" \033[31m•\033[0m {spec}")
|
|
179
|
+
|
|
180
|
+
print(f"\nBatch processing complete: {succeeded} succeeded, {failed} failed")
|
|
181
|
+
|
|
182
|
+
if failed > 0:
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
# Run black formatting after all codegen completes. Running black separately
|
|
186
|
+
# avoids duplicating black's import/startup cost in each worker process.
|
|
187
|
+
if output_dirs:
|
|
188
|
+
from pygen.black import BlackScriptPlugin
|
|
189
|
+
|
|
190
|
+
print(f"Formatting {len(output_dirs)} packages with black...")
|
|
191
|
+
for d in output_dirs:
|
|
192
|
+
BlackScriptPlugin(output_folder=d).process()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
freeze_support() # Required for Windows multiprocessing
|
|
197
|
+
main()
|