Skip to content

Commit 2451512

Browse files
Copilotl0lawrence
andcommitted
Add build_tests and create_samples skills to azpysdk CLI
Co-authored-by: l0lawrence <100643745+l0lawrence@users.noreply.github.com> Agent-Logs-Url: https://github.com/Azure/azure-sdk-for-python/sessions/6c11cb61-4032-4ed0-9cef-91775d2e9434
1 parent d9e40e4 commit 2451512

4 files changed

Lines changed: 356 additions & 4 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import argparse
2+
from typing import List, Optional
3+
4+
from .install_and_test import InstallAndTest
5+
from ci_tools.logging import logger
6+
7+
8+
class build_tests(InstallAndTest):
9+
"""Build and install a package's test environment without running pytest.
10+
11+
This check installs packaging tools, test tools (from eng/test_tools.txt),
12+
dev requirements, and builds/installs the package as a wheel — but does NOT
13+
invoke pytest or coverage. Useful for pre-validating the test environment in
14+
isolation (e.g. to surface dependency or build errors before running tests).
15+
"""
16+
17+
def __init__(self) -> None:
18+
super().__init__(
19+
package_type="wheel",
20+
proxy_url=None,
21+
display_name="build-tests",
22+
coverage_enabled=False,
23+
)
24+
25+
def register(
26+
self, subparsers: "argparse._SubParsersAction", parent_parsers: Optional[List[argparse.ArgumentParser]] = None
27+
) -> None:
28+
"""Register the ``build-tests`` subcommand.
29+
30+
The ``build-tests`` check installs packaging tools, test tools, dev
31+
requirements, and builds/installs the target package as a wheel without
32+
running pytest or coverage.
33+
"""
34+
parents = parent_parsers or []
35+
p = subparsers.add_parser(
36+
"build-tests",
37+
parents=parents,
38+
help=(
39+
"Build the test environment for a package (installs deps, builds wheel) "
40+
"without running pytest. Useful for pre-validating the test environment."
41+
),
42+
)
43+
p.set_defaults(func=self.run)
44+
45+
def run(self, args: argparse.Namespace) -> int:
46+
"""Install requirements and build/install the package; skip pytest and coverage."""
47+
import os
48+
import sys
49+
50+
logger.info("Running build-tests check...")
51+
52+
targeted = self.get_targeted_directories(args)
53+
if not targeted:
54+
logger.warning("No target packages discovered for build-tests check.")
55+
return 0
56+
57+
results: List[int] = []
58+
59+
for parsed in targeted:
60+
if os.getcwd() != parsed.folder:
61+
os.chdir(parsed.folder)
62+
package_dir = parsed.folder
63+
package_name = parsed.name
64+
65+
executable, staging_directory = self.get_executable(args.isolate, args.command, sys.executable, package_dir)
66+
logger.info(f"Processing {package_name} using interpreter {executable}")
67+
68+
install_result = self.install_all_requirements(
69+
executable, staging_directory, package_name, package_dir, args
70+
)
71+
if install_result != 0:
72+
logger.error(f"build-tests FAILED for {package_name} (exit code {install_result}).")
73+
results.append(install_result)
74+
else:
75+
logger.info(f"build-tests SUCCEEDED for {package_name}.")
76+
77+
return max(results) if results else 0
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import argparse
2+
import os
3+
from typing import List, Optional
4+
5+
from .Check import Check
6+
from ci_tools.logging import logger
7+
8+
_README_TEMPLATE = """\
9+
# {package_name} Samples
10+
11+
This directory contains samples for the `{package_name}` package.
12+
13+
## Getting started
14+
15+
Install the package and its dependencies:
16+
17+
```bash
18+
pip install {package_name}
19+
```
20+
21+
## Running the samples
22+
23+
```bash
24+
python sample_hello_world.py
25+
```
26+
27+
## Advanced code generation
28+
29+
To generate a fuller set of samples and SDK code from a TypeSpec or Swagger
30+
spec, use the `azpysdk generate` command:
31+
32+
```bash
33+
azpysdk generate <target>
34+
```
35+
36+
See `azpysdk generate --help` for more information.
37+
"""
38+
39+
_HELLO_WORLD_TEMPLATE = """\
40+
# ------------------------------------
41+
# Copyright (c) Microsoft Corporation.
42+
# Licensed under the MIT License.
43+
# ------------------------------------
44+
\"\"\"
45+
FILE: sample_hello_world.py
46+
47+
DESCRIPTION:
48+
A minimal sample for the {package_name} package.
49+
50+
To learn how to generate a fuller set of samples from a TypeSpec or
51+
Swagger spec, run:
52+
53+
azpysdk generate <target>
54+
\"\"\"
55+
56+
57+
def main():
58+
# TODO: replace with real usage of {package_name}
59+
print("Hello from {package_name}!")
60+
61+
62+
if __name__ == "__main__":
63+
main()
64+
"""
65+
66+
67+
class create_samples(Check):
68+
"""Generate a starter samples scaffold for a targeted package.
69+
70+
For each targeted package this check:
71+
72+
* Creates a ``samples/`` directory if one does not already exist.
73+
* Generates a starter ``README.md`` inside ``samples/`` (if absent).
74+
* Generates a starter ``sample_hello_world.py`` inside ``samples/`` (if absent).
75+
76+
For full SDK code generation (including samples) from a TypeSpec or Swagger
77+
spec, use the existing ``azpysdk generate`` command::
78+
79+
azpysdk generate <target>
80+
"""
81+
82+
def __init__(self) -> None:
83+
super().__init__()
84+
85+
def register(
86+
self, subparsers: "argparse._SubParsersAction", parent_parsers: Optional[List[argparse.ArgumentParser]] = None
87+
) -> None:
88+
"""Register the ``create-samples`` subcommand.
89+
90+
Creates a starter ``samples/`` scaffold (README + hello-world sample)
91+
for each targeted package. For full SDK code generation from a
92+
TypeSpec/Swagger spec use ``azpysdk generate <target>``.
93+
"""
94+
parents = parent_parsers or []
95+
p = subparsers.add_parser(
96+
"create-samples",
97+
parents=parents,
98+
help=(
99+
"Generate a starter samples scaffold (README.md, sample_hello_world.py) "
100+
"for each targeted package. For full code generation from a TypeSpec or "
101+
"Swagger spec, use: azpysdk generate <target>"
102+
),
103+
)
104+
p.set_defaults(func=self.run)
105+
p.add_argument(
106+
"--output-dir",
107+
default=None,
108+
help=(
109+
"Directory to write the samples scaffold into. "
110+
"Defaults to <package_dir>/samples."
111+
),
112+
)
113+
114+
def run(self, args: argparse.Namespace) -> int:
115+
"""Create a samples scaffold for each targeted package."""
116+
logger.info("Running create-samples check...")
117+
118+
targeted = self.get_targeted_directories(args)
119+
if not targeted:
120+
logger.warning("No target packages discovered for create-samples check.")
121+
return 0
122+
123+
results: List[int] = []
124+
125+
for parsed in targeted:
126+
package_dir = parsed.folder
127+
package_name = parsed.name
128+
129+
output_dir = getattr(args, "output_dir", None) or os.path.join(package_dir, "samples")
130+
131+
try:
132+
self._create_scaffold(package_name, output_dir)
133+
logger.info(
134+
f"create-samples SUCCEEDED for {package_name}. "
135+
f"Samples scaffold written to: {output_dir}"
136+
)
137+
logger.info(
138+
"Tip: for full SDK code generation from a TypeSpec or Swagger spec, run: "
139+
f"azpysdk generate {package_dir}"
140+
)
141+
except Exception as exc:
142+
logger.error(f"create-samples FAILED for {package_name}: {exc}")
143+
results.append(1)
144+
145+
return max(results) if results else 0
146+
147+
def _create_scaffold(self, package_name: str, output_dir: str) -> None:
148+
"""Create the samples directory and starter files."""
149+
os.makedirs(output_dir, exist_ok=True)
150+
151+
readme_path = os.path.join(output_dir, "README.md")
152+
if not os.path.exists(readme_path):
153+
with open(readme_path, "w", encoding="utf-8") as f:
154+
f.write(_README_TEMPLATE.format(package_name=package_name))
155+
logger.info(f"Created {readme_path}")
156+
else:
157+
logger.info(f"Skipping {readme_path} (already exists)")
158+
159+
hello_world_path = os.path.join(output_dir, "sample_hello_world.py")
160+
if not os.path.exists(hello_world_path):
161+
with open(hello_world_path, "w", encoding="utf-8") as f:
162+
f.write(_HELLO_WORLD_TEMPLATE.format(package_name=package_name))
163+
logger.info(f"Created {hello_world_path}")
164+
else:
165+
logger.info(f"Skipping {hello_world_path} (already exists)")

eng/tools/azure-sdk-tools/azpysdk/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@
3131
from .sdist import sdist
3232
from .whl_no_aio import whl_no_aio
3333
from .verify_whl import verify_whl
34+
from .build_tests import build_tests
3435
from .bandit import bandit
3536
from .verify_keywords import verify_keywords
3637
from .generate import generate
3738
from .breaking import breaking
3839
from .mindependency import mindependency
3940
from .latestdependency import latestdependency
4041
from .samples import samples
42+
from .create_samples import create_samples
4143
from .devtest import devtest
4244
from .optional import optional
4345
from .update_snippet import update_snippet
@@ -107,13 +109,15 @@ def build_parser() -> argparse.ArgumentParser:
107109
sdist().register(subparsers, [common])
108110
whl_no_aio().register(subparsers, [common])
109111
verify_whl().register(subparsers, [common])
112+
build_tests().register(subparsers, [common])
110113
bandit().register(subparsers, [common])
111114
verify_keywords().register(subparsers, [common])
112115
generate().register(subparsers, [common])
113116
breaking().register(subparsers, [common])
114117
mindependency().register(subparsers, [common])
115118
latestdependency().register(subparsers, [common])
116119
samples().register(subparsers, [common])
120+
create_samples().register(subparsers, [common])
117121
devtest().register(subparsers, [common])
118122
optional().register(subparsers, [common])
119123
update_snippet().register(subparsers, [common])

0 commit comments

Comments
 (0)