Skip to content

Commit ad3e092

Browse files
scbeddlmazuelsemick-dev
authored
Populating whl and imports entrypoint (#42677)
* Add Check class to be used as common definition for a check * add whl and import_all check entrypoints * refactor to allow automatic venv isolation * update readme Co-authored-by: Laurent Mazuel <[email protected]> Co-authored-by: Scott Beddall <[email protected]>
1 parent 57168a5 commit ad3e092

File tree

14 files changed

+493
-83
lines changed

14 files changed

+493
-83
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ src/build
107107

108108
# [begoldsm] ignore virtual env if it exists.
109109
adlEnv/
110-
venv/
111-
.venv/
110+
venv*
111+
.venv*
112112
code_reports
113113

114114
# Azure Storage test credentials

eng/tools/azure-sdk-tools/README.md

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ This package is intended for usage in direct combination with the azure-sdk-for-
2424

2525
| Module | Description |
2626
|---|---|
27+
| `azpysdk` | CI check entrypoints and accompanying implementations. |
28+
| `ci_tools` | Various azure-sdk-for-python specific build and test abstractions. Heavily used in CI functionality. |
2729
| `devtools_testutils` | Primary location for test classes, pytest fixtures, and test-proxy integration. |
28-
| `ci_tools` | Various azure-sdk-for-python specific build and test abstractions. |
2930
| `packaging_tools` | Templated package generator for management packages. |
31+
| `parsing` | Parse information _about_ python packages at rest on disk. Used to interrogate the monorepo to find relevant packages. |
3032
| `pypi_tools` | Helper functionality build upon interactions with PyPI. |
33+
| `scenario` | Functionality to do with installing packages constrained by environmental and input factors. Heavily used in `mindependency` and `latestdependency` CI checks. |
3134
| `testutils` | Backwards compatible extension of test classes. |
3235

3336
**PLEASE NOTE.** For the "script" entrypoints provided by the package, all should either be run from somewhere **within** the azure-sdk-for-python repository. Barring that, an argument `--repo` should be provided that points to the repo root if a user must start the command from a different CWD.
@@ -37,33 +40,39 @@ This package is intended for usage in direct combination with the azure-sdk-for-
3740
After installing azure-sdk-tools, package build functionality is available through `sdk_build`.
3841

3942
```text
40-
usage: sdk_build [-h] [-d DISTRIBUTION_DIRECTORY] [--service SERVICE] [--pkgfilter PACKAGE_FILTER_STRING] [--devbuild IS_DEV_BUILD]
41-
[--produce_apiview_artifact] [--repo REPO] [--build_id BUILD_ID]
43+
usage: sdk_build [-h] [-d DISTRIBUTION_DIRECTORY] [--service SERVICE] [--pkgfilter PACKAGE_FILTER_STRING]
44+
[--devbuild IS_DEV_BUILD] [--inactive] [--produce_apiview_artifact] [--repo REPO] [--build_id BUILD_ID]
4245
[glob_string]
4346
44-
This is the primary entrypoint for the "build" action. This command is used to build any package within the azure-sdk-for-python repository.
47+
This is the primary entrypoint for the "build" action. This command is used to build any package within the azure-sdk-for-
48+
python repository.
4549
4650
positional arguments:
47-
glob_string A comma separated list of glob strings that will target the top level directories that contain packages. Examples: All == "azure-*",
48-
Single = "azure-keyvault"
51+
glob_string A comma separated list of glob strings that will target the top level directories that contain
52+
packages. Examples: All == "azure-*", Single = "azure-keyvault"
4953
50-
optional arguments:
54+
options:
5155
-h, --help show this help message and exit
5256
-d DISTRIBUTION_DIRECTORY, --distribution-directory DISTRIBUTION_DIRECTORY
53-
The path to the distribution directory. Should be passed $(Build.ArtifactStagingDirectory) from the devops yaml definition.If that
54-
is not provided, will default to env variable SDK_ARTIFACT_DIRECTORY -> <calculated_repo_root>/.artifacts.
57+
The path to the distribution directory. Should be passed $(Build.ArtifactStagingDirectory) from the
58+
devops yaml definition.If that is not provided, will default to env variable SDK_ARTIFACT_DIRECTORY
59+
-> <calculated_repo_root>/.artifacts.
5560
--service SERVICE Name of service directory (under sdk/) to build.Example: --service applicationinsights
5661
--pkgfilter PACKAGE_FILTER_STRING
57-
An additional string used to filter the set of artifacts by a simple CONTAINS clause. This filters packages AFTER the set is built
58-
with compatibility and omission lists accounted.
62+
An additional string used to filter the set of artifacts by a simple CONTAINS clause. This filters
63+
packages AFTER the set is built with compatibility and omission lists accounted.
5964
--devbuild IS_DEV_BUILD
60-
Set build type to dev build so package requirements will be updated if required package is not available on PyPI
65+
Set build type to dev build so package requirements will be updated if required package is not
66+
available on PyPI
67+
--inactive Include inactive packages when assembling artifacts. CI builds will include inactive packages as a
68+
way to ensure that the yml controlled artifacts can be associated with a wheel/sdist.
6169
--produce_apiview_artifact
62-
Should an additional build artifact that contains the targeted package + its direct dependencies be produced?
63-
--repo REPO Where is the start directory that we are building against? If not provided, the current working directory will be used. Please
64-
ensure you are within the azure-sdk-for-python repository.
65-
--build_id BUILD_ID The current build id. It not provided, will default through environment variables in the following order: GITHUB_RUN_ID ->
66-
BUILD_BUILDID -> SDK_BUILD_ID -> default value.
70+
Should an additional build artifact that contains the targeted package + its direct dependencies be
71+
produced?
72+
--repo REPO Where is the start directory that we are building against? If not provided, the current working
73+
directory will be used. Please ensure you are within the azure-sdk-for-python repository.
74+
--build_id BUILD_ID The current build id. It not provided, will default through environment variables in the following
75+
order: GITHUB_RUN_ID -> BUILD_BUILDID -> SDK_BUILD_ID -> default value.
6776
```
6877

6978
Some common invocations.
@@ -149,6 +158,77 @@ optional arguments:
149158
ensure you are within the azure-sdk-for-python repository.
150159
```
151160

161+
## Using `Parsing` Modules
162+
163+
```python
164+
from ci_tools.parsing import ParsedSetup
165+
166+
path_to_package = "path/to/your/possible/python/package"
167+
168+
pkg_metadata = ParsedSetup.from_path(path_to_package)
169+
170+
# pkg_metadata will contain all metadata about the package, EG "name", "version", "python_requires", "dependencies"
171+
# any information that any of our checks would need to operate properly.
172+
```
173+
174+
## Writing additional `checks` within `azpysdk`
175+
176+
To add a new check to `azpysdk`, follow these steps.
177+
178+
- Create a new file within `azpysdk`. Name it after what your check will be.
179+
- When creating a class for your new check, you should inherit from the class `Check` within the `azpysdk` namespace.
180+
- Implement the required functions within the `Check` abstract class in your new check class.
181+
182+
This is a commented implementation providing the reasoning for implementation alongside the code.
183+
184+
```python
185+
class my_check(Check):
186+
def __init__(self) -> None:
187+
super().__init__()
188+
189+
190+
def register(self, subparsers: "argparse._SubParsersAction", parent_parsers: Optional[List[argparse.ArgumentParser]] = None) -> None:
191+
"""Register `my_check`. `my_check` does X and Y while doing Z.
192+
193+
This function will be used to add your command to the set of commands being shown through the `azpysdk` entrypoint.
194+
"""
195+
parents = parent_parsers or []
196+
p = subparsers.add_parser("whl", parents=parents, help="Run the `my_check` check")
197+
p.set_defaults(func=self.run)
198+
# Add any additional arguments specific to your check here (do not re-add common handled by parents. See `main.py` build_parser to see the common parents provided to the checks)
199+
200+
def run(self, args: argparse.Namespace) -> int:
201+
"""This is the recommended """
202+
set_envvar_defaults()
203+
204+
if args.target == ".":
205+
targeted = [os.getcwd()]
206+
else:
207+
target_dir = os.getcwd()
208+
targeted = discover_targeted_packages(args.target, target_dir)
209+
results: List[int] = []
210+
211+
for pkg in targeted:
212+
parsed = ParsedSetup.from_path(pkg)
213+
print(f"Processing {pkg.name} for my_check")
214+
```
215+
216+
- Once the new check has been created, register it in `azpysdk/main.py` on line 58. This will likely be automated by decorator in the near future but this is out how it should be done for now.
217+
218+
```python
219+
# ...other registrations above
220+
my_check().register(subparsers, [common])
221+
```
222+
223+
- Your command will now be available through `azpysdk`
224+
225+
```bash
226+
/> `azpysdk mycheck azure-storage*`
227+
228+
/> cd sdk/storage/azure-storage
229+
sdk/storage/azure-storage/> azpysdk mycheck
230+
```
231+
152232
## Relevant Environment Variables
153233

154234
This package honors a few different environment variables as far as logging, artifact placement, etc.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import abc
2+
import argparse
3+
from typing import Sequence, Optional, List, Any
4+
5+
class Check(abc.ABC):
6+
"""
7+
Base class for checks.
8+
9+
Subclasses must implement register() to add a subparser for the check.
10+
"""
11+
12+
def __init__(self) -> None:
13+
pass
14+
15+
@abc.abstractmethod
16+
def register(self, subparsers: "argparse._SubParsersAction", parent_parsers: Optional[List[argparse.ArgumentParser]] = None) -> None:
17+
"""
18+
Register this check with the CLI subparsers.
19+
20+
`subparsers` is the object returned by ArgumentParser.add_subparsers().
21+
`parent_parsers` can be a list of ArgumentParser objects to be used as parents.
22+
Subclasses MUST implement this method.
23+
"""
24+
raise NotImplementedError
25+
26+
def run(self, args: argparse.Namespace) -> int:
27+
"""Run the check command.
28+
29+
Subclasses can override this to perform the actual work.
30+
"""
31+
return 0
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import argparse
2+
import os
3+
import sys
4+
import logging
5+
import tempfile
6+
7+
from typing import Optional,List
8+
from subprocess import check_call
9+
10+
from .Check import Check
11+
from ci_tools.parsing import ParsedSetup
12+
from ci_tools.functions import discover_targeted_packages
13+
from ci_tools.scenario.generation import create_package_and_install
14+
15+
# keyvault has dependency issue when loading private module _BearerTokenCredentialPolicyBase from azure.core.pipeline.policies
16+
# azure.core.tracing.opencensus and azure.eventhub.checkpointstoreblob.aio are skipped due to a known issue in loading azure.core.tracing.opencensus
17+
excluded_packages = [
18+
"azure",
19+
"azure-mgmt",
20+
]
21+
22+
def should_run_import_all(package_name: str) -> bool:
23+
return not (package_name in excluded_packages or "nspkg" in package_name)
24+
25+
class import_all(Check):
26+
def __init__(self) -> None:
27+
super().__init__()
28+
29+
def register(self, subparsers: "argparse._SubParsersAction", parent_parsers: Optional[List[argparse.ArgumentParser]] = None) -> None:
30+
"""Register the `import_all` check. The import_all check checks dependencies of a package
31+
by installing just the package + its dependencies, then attempts to import * from the base namespace.
32+
33+
If it fails, there is a dependency being imported somewhere in the package namespace that doesn't
34+
exist in the `pyproject.toml`/`setup.py` dependencies. EG failure to import `isodate` within `azure.storage.blob.BlobClient`
35+
because `isodate` is not listed as a dependency for the package.
36+
"""
37+
parents = parent_parsers or []
38+
p = subparsers.add_parser("import_all", parents=parents, help="Run the import_all check")
39+
p.set_defaults(func=self.run)
40+
# Add any additional arguments specific to WhlCheck here (do not re-add common args)
41+
42+
# todo: figure out venv abstraction mechanism via override
43+
def run(self, args: argparse.Namespace) -> int:
44+
"""Run the import_all check command."""
45+
print("Running import_all check in isolated venv...")
46+
47+
# this is common. we should have an abstraction layer for this somehow
48+
if args.target == ".":
49+
targeted = [os.getcwd()]
50+
else:
51+
target_dir = os.getcwd()
52+
targeted = discover_targeted_packages(args.target, target_dir)
53+
54+
# {[tox]pip_command} freeze
55+
# python {repository_root}/eng/tox/import_all.py -t {tox_root}
56+
57+
outcomes: List[int] = []
58+
59+
for pkg in targeted:
60+
parsed = ParsedSetup.from_path(pkg)
61+
62+
staging_area = tempfile.mkdtemp()
63+
create_package_and_install(
64+
distribution_directory=staging_area,
65+
target_setup=pkg,
66+
skip_install=False,
67+
cache_dir=None,
68+
work_dir=staging_area,
69+
force_create=False,
70+
package_type="wheel",
71+
pre_download_disabled=False,
72+
)
73+
74+
if should_run_import_all(parsed.name):
75+
# import all modules from current package
76+
logging.info(
77+
"Importing all modules from namespace [{0}] to verify dependency".format(
78+
parsed.namespace
79+
)
80+
)
81+
import_script_all = "from {0} import *".format(parsed.namespace)
82+
commands = [
83+
sys.executable,
84+
"-c",
85+
import_script_all
86+
]
87+
88+
outcomes.append(check_call(commands))
89+
logging.info("Verified module dependency, no issues found")
90+
else:
91+
logging.info("Package {} is excluded from dependency check".format(parsed.name))
92+
93+
return max(outcomes) if outcomes else 0

0 commit comments

Comments
 (0)