You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ansible/hacking/update-sanity-requirements.py

185 lines
5.7 KiB
Python

#!/usr/bin/env python
# PYTHON_ARGCOMPLETE_OK
"""Generate frozen sanity test requirements from source requirements files."""
from __future__ import annotations
import argparse
import dataclasses
import pathlib
import re
import subprocess
import tempfile
import typing as t
import venv
import packaging.version
import packaging.specifiers
import packaging.requirements
try:
import argcomplete
except ImportError:
argcomplete = None
FILE = pathlib.Path(__file__).resolve()
ROOT = FILE.parent.parent
SELF = FILE.relative_to(ROOT)
@dataclasses.dataclass(frozen=True)
class SanityTest:
name: str
requirements_path: pathlib.Path
source_path: pathlib.Path
def freeze_requirements(self) -> None:
source_requirements = [packaging.requirements.Requirement(re.sub(' #.*$', '', line)) for line in self.source_path.read_text().splitlines()]
install_packages = {requirement.name for requirement in source_requirements}
exclude_packages = {'distribute', 'pip', 'setuptools', 'wheel'} - install_packages
with tempfile.TemporaryDirectory() as venv_dir:
venv.create(venv_dir, with_pip=True)
python = pathlib.Path(venv_dir, 'bin', 'python')
pip = [python, '-m', 'pip', '--disable-pip-version-check']
env = dict()
pip_freeze = subprocess.run(pip + ['freeze'], env=env, check=True, capture_output=True, text=True)
if pip_freeze.stdout:
raise Exception(f'Initial virtual environment is not empty:\n{pip_freeze.stdout}')
subprocess.run(pip + ['install', 'wheel'], env=env, check=True) # make bdist_wheel available during pip install
subprocess.run(pip + ['install', '-r', self.source_path], env=env, check=True)
[stable-2.14] Docs removal and other backports (#81407) * Remove straight.plugin dependency (#80084) (cherry picked from commit f587856beb4afa11040418ecf83b0bfd3d528ab6) * Update package-data sanity test (#80344) The test no longer relies on the Makefile. (cherry picked from commit 46362bbd2783d25ddc1edaaf4db7d00627ad7e88) * Remove obsolete release bits (#80347) Releases are now built using the `packaging/release.py` tool. This makes the `Makefile` and associated files in `packaging/release/` and `packaging/sdist/` obsolete. * Use --no-isolation for package-data sanity test (#80377) The dependencies are already in the sanity test venv. This avoids use of unpinned dependencies and a dependency on a network connection. (cherry picked from commit 7fcb9960e65591b42c1b46811dd529bae52ddf85) * Set the minimum setuptools to 45.2.0 (#80649) Also update the package-data sanity test to use the minimum setuptools version. (cherry picked from commit 4d25e3d54f7de316c4f1d1575d2cf1ffa46b632c) * Use package_data instead of include_package_data (#80652) This resolves warnings generated by setuptools such as the following: _Warning: Package 'ansible.galaxy.data' is absent from the `packages` configuration. (cherry picked from commit 5ac292e12d5e1515beb34028346d76bb68398fc8) * Fix os.walk issues in package-data sanity test (#80703) * Remove `docs` and `examples` directories (#81011) * Remove docs dir * Updates to reflect docs removal * Fix integration test * Remove examples dir * Updates to reflect examples removal * Remove build_library and build-ansible.py * Remove refs to build_library and build-ansible.py * Remove obsolete template * Remove obsolete template reference * Remove the now obsolete rstcheck sanity test (cherry picked from commit 72e038e8234051b54552d794a22ebef9681ae3ae) * Omit pre-built man pages from sdist (#81395) Since man pages aren't accessible to users after a `pip install`, there's no need to include them in the sdist. This change makes it trivial to build man pages from source, which makes them much easier to iterate on. It also simplifies creation and testing of the sdist, since it no longer requires building man pages. The new `packaging/cli-doc/build.py` script can generate both man pages and RST documentation. This supports inclusion on the docs site without a dependency on `ansible-core` internals. Having a single implementation for both simplifies keeping the two formats in sync. (cherry picked from commit 691c8e86034f1fe099e4ef54880e633b34f0bc7a)
1 year ago
freeze_options = ['--all']
for exclude_package in exclude_packages:
freeze_options.extend(('--exclude', exclude_package))
pip_freeze = subprocess.run(pip + ['freeze'] + freeze_options, env=env, check=True, capture_output=True, text=True)
self.write_requirements(pip_freeze.stdout)
def update_pre_build(self) -> None:
"""Update requirements in place with current pre-build instructions."""
requirements = pathlib.Path(self.requirements_path).read_text()
lines = requirements.splitlines(keepends=True)
lines = [line for line in lines if not line.startswith('#')]
requirements = ''.join(lines)
self.write_requirements(requirements)
def write_requirements(self, requirements: str) -> None:
"""Write the given test requirements to the requirements file for this test."""
pre_build = pre_build_instructions(requirements)
requirements = f'# edit "{self.source_path.name}" and generate with: {SELF} --test {self.name}\n{pre_build}{requirements}'
with open(self.requirements_path, 'w') as requirement_file:
requirement_file.write(requirements)
@staticmethod
def create(path: pathlib.Path) -> SanityTest:
return SanityTest(
name=path.stem.replace('sanity.', '').replace('.requirements', ''),
requirements_path=path,
source_path=path.with_suffix('.in'),
)
def pre_build_instructions(requirements: str) -> str:
"""Parse the given requirements and return any applicable pre-build instructions."""
parsed_requirements = requirements.splitlines()
package_versions = {
match.group('package').lower(): match.group('version') for match
in (re.search('^(?P<package>.*)==(?P<version>.*)$', requirement) for requirement in parsed_requirements)
if match
}
instructions: list[str] = []
build_constraints = (
('pyyaml', '>= 5.4, <= 6.0', ('Cython < 3.0',)),
)
for package, specifier, constraints in build_constraints:
version_string = package_versions.get(package)
if version_string:
version = packaging.version.Version(version_string)
specifier_set = packaging.specifiers.SpecifierSet(specifier)
if specifier_set.contains(version):
instructions.append(f'# pre-build requirement: {package} == {version}\n')
for constraint in constraints:
instructions.append(f'# pre-build constraint: {constraint}\n')
return ''.join(instructions)
def main() -> None:
tests = find_tests()
parser = argparse.ArgumentParser()
parser.add_argument(
'--test',
metavar='TEST',
dest='test_names',
action='append',
choices=[test.name for test in tests],
help='test requirements to update'
)
parser.add_argument(
'--pre-build-only',
action='store_true',
help='apply pre-build instructions to existing requirements',
)
if argcomplete:
argcomplete.autocomplete(parser)
args = parser.parse_args()
test_names: set[str] = set(args.test_names or [])
tests = [test for test in tests if test.name in test_names] if test_names else tests
for test in tests:
print(f'===[ {test.name} ]===', flush=True)
if args.pre_build_only:
test.update_pre_build()
else:
test.freeze_requirements()
def find_tests() -> t.List[SanityTest]:
globs = (
'test/lib/ansible_test/_data/requirements/sanity.*.txt',
'test/sanity/code-smell/*.requirements.txt',
)
tests: t.List[SanityTest] = []
for glob in globs:
tests.extend(get_tests(pathlib.Path(glob)))
return sorted(tests, key=lambda test: test.name)
def get_tests(glob: pathlib.Path) -> t.List[SanityTest]:
path = pathlib.Path(ROOT, glob.parent)
pattern = glob.name
return [SanityTest.create(item) for item in path.glob(pattern)]
if __name__ == '__main__':
main()