Skip to content

Commit da947a9

Browse files
scbeddlmazuelmccoyp
authored
Add Optional Environment (Azure#32332)
* updates to eng/ci_tools and eng/test_tools.txt to allow necessary additions for compatibility * added new namespace to azure-sdk-tools: ci_tools.scenario. * ci_tools.scenario is used to emplace any code that actually does the action of installing a dependency, replacing requirements, building on the fly, etc. This means that create_package_and_install and replace_dev_reqs both are moved into this module * created tox environment 'optional'. This tox environment utilizes array items of pyproject.toml config item 'tool.azure-sdk-build.optional' to install requirements for specific test scenarios before invoking tests against them. These tests are run within a custom venv for each optional configuration item in pyproject.toml, location .tox/optional/<envname>/ * the new 'optional' environment is enhanced to allow users to pass in the target environment EG: 'tox -e optional --root . -c ../../../eng/tox/tox.ini -- --optional no_requests' Co-authored-by: Laurent Mazuel <laurent.mazuel@gmail.com> Co-authored-by: McCoy Patiño <39780829+mccoyp@users.noreply.github.com>
1 parent c31596f commit da947a9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+995
-456
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ conda/assembly/
5959
conda/assembled/
6060
conda/downloaded/
6161
conda/conda-env/
62+
scenario_*.txt
6263

6364
# tox environment folders
6465
.tox/

.vscode/cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
"conda/conda-releaselogs/azure-mgmt.md"
128128
],
129129
"words": [
130+
"spinup",
130131
"cibuildwheel",
131132
"aoai",
132133
"pyprojects",

eng/tox/create_dependencies_and_install.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import os
1212
import logging
1313
from ci_tools.variables import in_ci
14-
from ci_tools.functions import build_and_install_dev_reqs
14+
from ci_tools.scenario.generation import build_and_install_dev_reqs
1515
logging.getLogger().setLevel(logging.INFO)
1616

1717

eng/tox/create_package_and_install.py

Lines changed: 15 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -9,83 +9,12 @@
99
# it should be executed from tox with `{toxenvdir}/python` to ensure that the package
1010
# can be successfully tested from within a tox environment.
1111

12-
from subprocess import check_call, CalledProcessError
1312
import argparse
14-
import os
1513
import logging
16-
import sys
17-
import glob
18-
import shutil
19-
20-
from tox_helper_tasks import get_pip_list_output
21-
from ci_tools.parsing import ParsedSetup, parse_require
22-
from ci_tools.build import create_package
23-
from ci_tools.functions import get_package_from_repo, find_whl, find_sdist, discover_prebuilt_package
2414

2515
logging.getLogger().setLevel(logging.INFO)
2616

27-
from ci_tools.parsing import ParsedSetup
28-
29-
30-
def cleanup_build_artifacts(build_folder):
31-
# clean up egginfo
32-
results = glob.glob(os.path.join(build_folder, "*.egg-info"))
33-
34-
if results:
35-
print(results[0])
36-
shutil.rmtree(results[0])
37-
38-
# clean up build results
39-
build_path = os.path.join(build_folder, "build")
40-
if os.path.exists(build_path):
41-
shutil.rmtree(build_path)
42-
43-
44-
def discover_packages(setuppy_path, args):
45-
packages = []
46-
if os.getenv("PREBUILT_WHEEL_DIR") is not None and not args.force_create:
47-
packages = discover_prebuilt_package(os.getenv("PREBUILT_WHEEL_DIR"), setuppy_path, args.package_type)
48-
pkg = ParsedSetup.from_path(setuppy_path)
49-
50-
if not packages:
51-
logging.error(
52-
"Package is missing in prebuilt directory {0} for package {1} and version {2}".format(
53-
os.getenv("PREBUILT_WHEEL_DIR"), pkg.name, pkg.version
54-
)
55-
)
56-
exit(1)
57-
else:
58-
packages = build_and_discover_package(
59-
setuppy_path,
60-
args.distribution_directory,
61-
args.target_setup,
62-
args.package_type,
63-
)
64-
65-
return packages
66-
67-
68-
def in_ci():
69-
return os.getenv("TF_BUILD", False)
70-
71-
72-
def build_and_discover_package(setuppy_path, dist_dir, target_setup, package_type):
73-
if package_type == "wheel":
74-
create_package(setuppy_path, dist_dir, enable_sdist=False)
75-
else:
76-
create_package(setuppy_path, dist_dir, enable_wheel=False)
77-
78-
prebuilt_packages = [
79-
f
80-
for f in os.listdir(args.distribution_directory)
81-
if f.endswith(".whl" if package_type == "wheel" else ".tar.gz")
82-
]
83-
84-
if not in_ci():
85-
logging.info("Cleaning up build directories and files")
86-
cleanup_build_artifacts(target_setup)
87-
return prebuilt_packages
88-
17+
from ci_tools.scenario.generation import create_package_and_install
8918

9019
if __name__ == "__main__":
9120
parser = argparse.ArgumentParser(
@@ -119,19 +48,22 @@ def build_and_discover_package(setuppy_path, dist_dir, target_setup, package_typ
11948
"--cache-dir",
12049
dest="cache_dir",
12150
help="Location that, if present, will be used as the pip cache directory.",
51+
default=None
12252
)
12353

12454
parser.add_argument(
12555
"-w",
12656
"--work-dir",
12757
dest="work_dir",
12858
help="Location that, if present, will be used as working directory to run pip install.",
59+
default=None
12960
)
13061

13162
parser.add_argument(
13263
"--force-create",
13364
dest="force_create",
13465
help="Force recreate whl even if it is prebuilt",
66+
default=False
13567
)
13668

13769
parser.add_argument(
@@ -150,117 +82,16 @@ def build_and_discover_package(setuppy_path, dist_dir, target_setup, package_typ
15082

15183
args = parser.parse_args()
15284

153-
commands_options = []
154-
built_pkg_path = ""
155-
setup_py_path = os.path.join(args.target_setup, "setup.py")
156-
additional_downloaded_reqs = []
157-
158-
if not os.path.exists(args.distribution_directory):
159-
os.mkdir(args.distribution_directory)
160-
161-
tmp_dl_folder = os.path.join(args.distribution_directory, "dl")
162-
if not os.path.exists(tmp_dl_folder):
163-
os.mkdir(tmp_dl_folder)
164-
165-
# preview version is enabled when installing dev build so pip will install dev build version from devpos feed
166-
if os.getenv("SetDevVersion", "false") == "true":
167-
commands_options.append("--pre")
168-
169-
if args.cache_dir:
170-
commands_options.extend(["--cache-dir", args.cache_dir])
171-
172-
discovered_packages = discover_packages(setup_py_path, args)
173-
174-
if args.skip_install:
175-
logging.info("Flag to skip install whl is passed. Skipping package installation")
176-
else:
177-
for built_package in discovered_packages:
178-
if os.getenv("PREBUILT_WHEEL_DIR") is not None and not args.force_create:
179-
# find the prebuilt package in the set of prebuilt wheels
180-
package_path = os.path.join(os.environ["PREBUILT_WHEEL_DIR"], built_package)
181-
if os.path.isfile(package_path):
182-
built_pkg_path = package_path
183-
logging.info("Installing {w} from directory".format(w=built_package))
184-
# it does't exist, so we need to error out
185-
else:
186-
logging.error("{w} not present in the prebuilt package directory. Exiting.".format(w=built_package))
187-
exit(1)
188-
else:
189-
built_pkg_path = os.path.abspath(os.path.join(args.distribution_directory, built_package))
190-
logging.info("Installing {w} from fresh built package.".format(w=built_package))
191-
192-
if not args.pre_download_disabled:
193-
requirements = ParsedSetup.from_path(
194-
os.path.join(os.path.abspath(args.target_setup), "setup.py")
195-
).requires
196-
azure_requirements = [req.split(";")[0] for req in requirements if req.startswith("azure-")]
197-
198-
if azure_requirements:
199-
logging.info(
200-
"Found {} azure requirement(s): {}".format(len(azure_requirements), azure_requirements)
201-
)
202-
203-
download_command = [
204-
sys.executable,
205-
"-m",
206-
"pip",
207-
"download",
208-
"-d",
209-
tmp_dl_folder,
210-
"--no-deps",
211-
]
212-
213-
installation_additions = []
214-
215-
# only download a package if the requirement is not already met, so walk across
216-
# direct install_requires
217-
for req in azure_requirements:
218-
addition_necessary = True
219-
# get all installed packages
220-
installed_pkgs = get_pip_list_output()
221-
222-
# parse the specifier
223-
req_name, req_specifier = parse_require(req)
224-
225-
# if we have the package already present...
226-
if req_name in installed_pkgs:
227-
# if there is no specifier for the requirement, we can ignore it
228-
if req_specifier is None:
229-
addition_necessary = False
230-
231-
# ...do we need to install the new version? if the existing specifier matches, we're fine
232-
if req_specifier is not None and installed_pkgs[req_name] in req_specifier:
233-
addition_necessary = False
234-
235-
if addition_necessary:
236-
# we only want to add an additional rec for download if it actually exists
237-
# in the upstream feed (either dev or pypi)
238-
# if it doesn't, we should just install the relative dep if its an azure package
239-
installation_additions.append(req)
240-
241-
if installation_additions:
242-
non_present_reqs = []
243-
for addition in installation_additions:
244-
try:
245-
check_call(
246-
download_command + [addition] + commands_options,
247-
env=dict(os.environ, PIP_EXTRA_INDEX_URL=""),
248-
)
249-
except CalledProcessError as e:
250-
req_name, req_specifier = parse_require(addition)
251-
non_present_reqs.append(req_name)
252-
253-
additional_downloaded_reqs = [
254-
os.path.abspath(os.path.join(tmp_dl_folder, pth)) for pth in os.listdir(tmp_dl_folder)
255-
] + [get_package_from_repo(relative_req).folder for relative_req in non_present_reqs]
85+
create_package_and_install(
86+
distribution_directory=args.distribution_directory,
87+
target_setup=args.target_setup,
88+
skip_install=args.skip_install,
89+
cache_dir=args.cache_dir,
90+
work_dir=args.work_dir,
91+
force_create=args.force_create,
92+
package_type=args.package_type,
93+
pre_download_disabled=args.pre_download_disabled,
94+
)
25695

257-
commands = [sys.executable, "-m", "pip", "install", built_pkg_path]
258-
commands.extend(additional_downloaded_reqs)
259-
commands.extend(commands_options)
26096

261-
if args.work_dir and os.path.exists(args.work_dir):
262-
logging.info("Executing command from {0}:{1}".format(args.work_dir, commands))
263-
check_call(commands, cwd=args.work_dir)
264-
else:
265-
check_call(commands)
266-
logging.info("Installed {w}".format(w=built_package))
97+

eng/tox/run_optional.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import argparse
2+
from ci_tools.scenario.generation import prepare_and_test_optional
3+
4+
5+
if __name__ == "__main__":
6+
parser = argparse.ArgumentParser(
7+
description="""This entrypoint provides automatic invocation of the 'optional' requirements for a given package. View the pyproject.toml within the targeted package folder to see configuration.""",
8+
)
9+
10+
parser.add_argument("-t", "--target", dest="target", help="The target package path", required=True)
11+
12+
parser.add_argument(
13+
"-o",
14+
"--optional",
15+
dest="optional",
16+
help="The target environment. If not provided, all optional environments will be run.",
17+
required=False,
18+
)
19+
20+
parser.add_argument(
21+
"--temp",
22+
dest="temp_dir",
23+
help="The temp directory this script will work in.",
24+
required=False,
25+
)
26+
27+
args, _ = parser.parse_known_args()
28+
exit(prepare_and_test_optional(mapped_args=args))

eng/tox/tox.ini

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ deps=
539539
commands=
540540
black --config {repository_root}/eng/black-pyproject.toml {posargs}
541541

542+
542543
[testenv:generate]
543544
description=Regenerate the code
544545
skip_install=true
@@ -547,3 +548,16 @@ deps =
547548
tomli==2.0.1
548549
commands=
549550
python -m packaging_tools.generate_client
551+
552+
553+
[testenv:optional]
554+
skipsdist = true
555+
skip_install = true
556+
usedevelop = true
557+
changedir = {toxinidir}
558+
setenv =
559+
{[testenv]setenv}
560+
PROXY_URL=http://localhost:5018
561+
commands =
562+
{envbindir}/python -m pip install {toxinidir}/../../../tools/azure-sdk-tools[build]
563+
python {repository_root}/eng/tox/run_optional.py -t {toxinidir} --temp={envtmpdir} {posargs}

eng/tox/tox_helper_tasks.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,6 @@
1919

2020
logging.getLogger().setLevel(logging.INFO)
2121

22-
23-
def get_pip_list_output():
24-
"""Uses the invoking python executable to get the output from pip list."""
25-
out = subprocess.Popen(
26-
[sys.executable, "-m", "pip", "list", "--disable-pip-version-check", "--format", "freeze"],
27-
stdout=subprocess.PIPE,
28-
stderr=subprocess.STDOUT,
29-
)
30-
31-
stdout, stderr = out.communicate()
32-
33-
collected_output = {}
34-
35-
if stdout and (stderr is None):
36-
# this should be compatible with py27 https://docs.python.org/2.7/library/stdtypes.html#str.decode
37-
for line in stdout.decode("utf-8").split(os.linesep)[2:]:
38-
if line:
39-
package, version = re.split("==", line)
40-
collected_output[package] = version
41-
else:
42-
raise Exception(stderr)
43-
44-
return collected_output
45-
46-
4722
def unzip_sdist_to_directory(containing_folder: str) -> str:
4823
zips = glob.glob(os.path.join(containing_folder, "*.zip"))
4924

scripts/devops_tasks/tox_harness.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
from ci_tools.variables import in_ci
1919
from ci_tools.environment_exclusions import filter_tox_environment_string
2020
from ci_tools.ci_interactions import output_ci_warning
21-
from ci_tools.functions import build_whl_for_req, cleanup_directory, replace_dev_reqs
22-
from pkg_resources import parse_requirements
21+
from ci_tools.scenario.generation import replace_dev_reqs
22+
from ci_tools.functions import cleanup_directory
23+
from pkg_resources import parse_requirements, RequirementParseError
2324
import logging
2425

2526
logging.getLogger().setLevel(logging.INFO)

sdk/core/azure-core/pyproject.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,16 @@ strict_sphinx = true
77
# For test environments or static checks where a check should be run by default, not explicitly disabling will enable the check.
88
# pylint is enabled by default, so there is no reason for a pylint = true in every pyproject.toml.
99
#
10-
# For newly added checks that are not enabled by default, packages should opt IN by "<check> = true".
10+
# For newly added checks that are not enabled by default, packages should opt IN by "<check> = true".
11+
12+
[[tool.azure-sdk-build.optional]]
13+
name = "no_requests"
14+
install = []
15+
uninstall = ["requests"]
16+
additional_pytest_args = ["--ignore-glob='*_async.py'"]
17+
18+
[[tool.azure-sdk-build.optional]]
19+
name = "no_aiohttp"
20+
install = []
21+
uninstall = ["aiohttp"]
22+
additional_pytest_args = ["-k", "_async.py"]

0 commit comments

Comments
 (0)