diff --git a/backports.txt b/backports.txt deleted file mode 100644 index d0437a41d..000000000 --- a/backports.txt +++ /dev/null @@ -1 +0,0 @@ -importlib-metadata diff --git a/doc/source/conf.py b/doc/source/conf.py index ee761f553..0891c2fb7 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,7 +1,7 @@ -# -- General configuration ----------------------------------------------------- +# -- General configuration ---------------------------------------------------- -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['openstackdocstheme'] todo_include_todos = True @@ -30,7 +30,7 @@ # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['requirements-doc.'] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -42,10 +42,10 @@ # If false, no index is generated. html_use_index = False -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (index file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( 'index', @@ -56,7 +56,7 @@ ), ] -# -- Options for openstackdocstheme extension ---------------------------------- +# -- Options for openstackdocstheme extension --------------------------------- openstackdocs_repo_name = 'openstack/requirements' openstackdocs_auto_name = False diff --git a/global-requirements.txt b/global-requirements.txt index 663c31a4a..5455bccac 100644 --- a/global-requirements.txt +++ b/global-requirements.txt @@ -165,7 +165,6 @@ scipy # BSD semantic-version # BSD setproctitle # BSD SQLAlchemy # MIT -sqlalchemy-filters # Apache-2.0 SQLAlchemy-Utils # BSD License sshtunnel # MIT statsd # MIT diff --git a/openstack_requirements/check.py b/openstack_requirements/check.py index c6a5d6637..ed1444ea3 100644 --- a/openstack_requirements/check.py +++ b/openstack_requirements/check.py @@ -18,32 +18,26 @@ import re import sys -from packaging import markers - from openstack_requirements.project import Project from openstack_requirements import requirement -MIN_PY_VERSION = '3.5' PY3_GLOBAL_SPECIFIER_RE = re.compile( r'python_version(==|>=|>)[\'"]3\.\d+[\'"]' ) PY3_LOCAL_SPECIFIER_RE = re.compile( r'python_version(==|>=|>|<=|<)[\'"]3\.\d+[\'"]' ) +WINDOWS_SPECIFIER_RE = re.compile(r'sys_platform!=[\'"]win32[\'"]') class RequirementsList: def __init__(self, name: str, project: Project) -> None: self.name = name self.reqs_by_file: dict[str, dict[str, set[str]]] = {} + self.optional_reqs_by_file: dict[str, dict[str, set[str]]] = {} self.project = project self.failed = False - @property - def reqs(self) -> dict[str, set[str]]: - """Flattens the list of per-file reqs.""" - return {k: v for d in self.reqs_by_file.values() for k, v in d.items()} - def extract_reqs( self, content: list[str], strict: bool ) -> dict[str, set[str]]: @@ -112,7 +106,7 @@ def process(self, strict: bool = True) -> None: print(f"Processing {fname} (extras)") for name, content in extras.items(): print(f" Processing {name!r} extra") - self.reqs_by_file[f'{fname} ({name!r} extra)'] = ( + self.optional_reqs_by_file[f'{fname} ({name!r} extra)'] = ( self.extract_reqs(content, strict) ) @@ -120,9 +114,9 @@ def process(self, strict: bool = True) -> None: print(f"Processing {fname} (dependency-groups)") for name, content in groups.items(): print(f" Processing {name!r} dependency group") - self.reqs_by_file[f'{fname} ({name!r} dependency group)'] = ( - self.extract_reqs(content, strict) - ) + self.optional_reqs_by_file[ + f'{fname} ({name!r} dependency group)' + ] = self.extract_reqs(content, strict) def _get_exclusions(req): @@ -136,7 +130,6 @@ def _get_exclusions(req): def _is_requirement_in_global_reqs( local_req, global_reqs, - backports, ): req_exclusions = _get_exclusions(local_req) for global_req in global_reqs: @@ -145,14 +138,18 @@ def _is_requirement_in_global_reqs( local_req_val = getattr(local_req, aname) global_req_val = getattr(global_req, aname) if local_req_val != global_req_val: - # if a python 3 version is not specified in only one of - # global requirements or local requirements, allow it since - # python 3-only is okay if matching and aname == 'markers': + # if a Python version marker is specified globally but not + # locally, allow it since this is unnecessary boilerplate + # for projects to carry if not local_req_val and PY3_GLOBAL_SPECIFIER_RE.match( global_req_val ): continue + + # if a Python version marker is specified locally but not + # globally, allow it since projects might only need the + # package on specific Python versions if ( not global_req_val and local_req_val @@ -160,34 +157,21 @@ def _is_requirement_in_global_reqs( ): continue - # likewise, if a package is one of the backport packages then - # we're okay with a potential marker (e.g. if a package - # requires a feature that is only available in a newer Python - # library, while other packages are happy without this feature - if ( - matching - and aname == 'markers' - and local_req.package in backports - ): - if re.match( - r'python_version(==|<=|<)[\'"]3\.\d+[\'"]', - local_req_val, + # OpenStack no longer supports Windows. If a package wants + # to drop their sys_platform marker, let them. + if not local_req_val and WINDOWS_SPECIFIER_RE.match( + global_req_val ): - print( - 'Ignoring backport package with python_version ' - 'marker' - ) continue print( - f'WARNING: possible mismatch found for package "{local_req.package}"' - ) # noqa: E501 - print(f' Attribute "{aname}" does not match') - print( - f' "{local_req_val}" does not match "{global_req_val}"' - ) # noqa: E501 - print(f' {local_req}') - print(f' {global_req}') + f'WARNING: possible mismatch found for package ' + f'{local_req.package!r}\n' + f' Attribute "{aname}" does not match\n' + f' "{local_req_val}" does not match "{global_req_val}"\n' + f' {local_req}\n' + f' {global_req}' + ) matching = False if not matching: continue @@ -202,8 +186,7 @@ def _is_requirement_in_global_reqs( difference = req_exclusions - global_exclusions print( f"ERROR: Requirement for package {local_req.package} " - f"excludes a version not excluded in the " - f"global list.\n" + f"excludes a version not excluded in the global list.\n" f" Local settings : {list(req_exclusions)}\n" f" Global settings: {list(global_exclusions)}\n" f" Unexpected : {list(difference)}" @@ -234,29 +217,13 @@ def get_global_reqs(content): return global_reqs -def _get_python3_reqs(reqs): - """Filters out the reqs that are less than our minimum version.""" - results = [] - for req in reqs: - if not req.markers: - results.append(req) - else: - req_markers = markers.Marker(req.markers) - if req_markers.evaluate( - { - 'python_version': MIN_PY_VERSION, - } - ): - results.append(req) - return results - - def _validate_one( name, reqs, denylist, global_reqs, - backports, + *, + is_optional, ): """Returns True if there is a failure.""" @@ -267,6 +234,9 @@ def _validate_one( return False if name not in global_reqs: + if is_optional: + return False + print(f"ERROR: Requirement '{reqs}' not in openstack/requirements") return True @@ -278,11 +248,7 @@ def _validate_one( else: counts[''] = counts.get('', 0) + 1 - if not _is_requirement_in_global_reqs( - req, - global_reqs[name], - backports, - ): + if not _is_requirement_in_global_reqs(req, global_reqs[name]): return True # check for minimum being defined @@ -314,23 +280,29 @@ def validate( head_reqs, denylist, global_reqs, - backports, ): failed = False # iterate through the changing entries and see if they match the global # equivalents we want enforced - for fname, freqs in head_reqs.reqs_by_file.items(): - print(f"Validating {fname}") - for name, reqs in freqs.items(): - failed = ( - _validate_one( - name, - reqs, - denylist, - global_reqs, - backports, + # note that extras and dependency groups are project-specific and may not + # be present in global-requirements, so we only note their absence rather + # than failing if so + for reqs_by_file, is_optional in ( + (head_reqs.reqs_by_file, False), + (head_reqs.optional_reqs_by_file, True), + ): + for fname, freqs in reqs_by_file.items(): + print(f"Validating {fname}") + for name, reqs in freqs.items(): + failed = ( + _validate_one( + name, + reqs, + denylist, + global_reqs, + is_optional=is_optional, + ) + or failed ) - or failed - ) return failed diff --git a/openstack_requirements/cmds/check_exists.py b/openstack_requirements/cmds/check_exists.py index dd2d5c680..95a989cbc 100644 --- a/openstack_requirements/cmds/check_exists.py +++ b/openstack_requirements/cmds/check_exists.py @@ -65,7 +65,8 @@ def main(args=None): for require_file, data in project_data.get('requirements', {}).items(): print( - f'\nComparing {require_file} with global-requirements and upper-constraints' + f'\nComparing {require_file} with global-requirements and ' + f'upper-constraints' ) requirements = requirement.parse_lines(data) for name, spec_list in requirements.items(): @@ -73,13 +74,15 @@ def main(args=None): continue if name not in global_requirements: print( - f'{name} from {require_file} not found in global-requirements' + f'{name} from {require_file} not found in ' + f'global-requirements' ) error_count += 1 continue if name not in upper_constraints: print( - f'{name} from {require_file} not found in upper-constraints' + f'{name} from {require_file} not found in ' + f'upper-constraints' ) error_count += 1 continue @@ -95,8 +98,9 @@ def main(args=None): # then something is wrong. if Version(uc_spec.version) not in specs: print( - f'{name} must be <= {uc_spec.version} from upper-constraints and ' - 'include the upper-constraints version' + f'{name} must be <= {uc_spec.version} from ' + f'upper-constraints and include the ' + f'upper-constraints version' ) error_count += 1 continue @@ -109,8 +113,9 @@ def main(args=None): continue if spec.version not in spec_gr: print( - f'Specifier {spec.version} from {name} is failing check ' - f'from global-requirements specifiers {str(spec_gr)}' + f'Specifier {spec.version} from {name} is ' + f'failing check from global-requirements ' + f'specifiers {str(spec_gr)}' ) error_count += 1 continue diff --git a/openstack_requirements/constraints.py b/openstack_requirements/constraints.py index f06d0e0fe..3adce31dc 100644 --- a/openstack_requirements/constraints.py +++ b/openstack_requirements/constraints.py @@ -67,7 +67,8 @@ def check_format(parsed_constraints): for req, original_line in spec_list: if not req.specifiers.startswith('==='): yield ( - f'Invalid constraint for {name} does not have 3 "=": {original_line}' + f'Invalid constraint for {name} does not have 3 "=": ' + f'{original_line}' ) @@ -106,7 +107,8 @@ def satisfied(reqs, name, version, failures): return True tested.append(constraint.specifiers) failures.append( - f'Constraint {version} for {name} does not match requirement {tested}' + f'Constraint {version} for {name} does not match requirement ' + f'{tested}' ) return False diff --git a/openstack_requirements/project.py b/openstack_requirements/project.py index 6924a9c08..f0bad78b1 100644 --- a/openstack_requirements/project.py +++ b/openstack_requirements/project.py @@ -20,16 +20,10 @@ import io import os import sys +import tomllib from typing import Any from typing import TypedDict -try: - # Python 3.11+ - import tomllib -except ImportError: - # Python 3.10 and lower - import tomli as tomllib # type: ignore - def _read_raw(root: str, filename: str) -> str | None: try: @@ -164,10 +158,14 @@ def verify_pyproject_toml(root: str) -> bool: return False if 'build-system' not in data: - print("pyproject.toml is missing 'build-system' table", file=sys.stderr) + print( + "pyproject.toml is missing 'build-system' table", file=sys.stderr + ) return False - if (build_backend := data['build-system'].get('build-backend')) != 'pbr.build': + if ( + build_backend := data['build-system'].get('build-backend') + ) != 'pbr.build': print( f"pyproject.toml has invalid 'build-system.build-backend'. " f"Expected 'pbr.build'; got {build_backend!r}", diff --git a/openstack_requirements/project_config.py b/openstack_requirements/project_config.py index 23c91704d..71831df0d 100644 --- a/openstack_requirements/project_config.py +++ b/openstack_requirements/project_config.py @@ -54,6 +54,7 @@ def require_check_requirements_for_repo(zuul_projects, repo): # jobs, because we want projects to use the templates. if 'check-requirements' not in templates: errors.append( - f'{ZUUL_PROJECTS_FILENAME} no check-requirements job specified for {repo}' + f'{ZUUL_PROJECTS_FILENAME} no check-requirements job ' + f'specified for {repo}' ) return errors diff --git a/openstack_requirements/requirement.py b/openstack_requirements/requirement.py index 40e4ea325..020d4048a 100644 --- a/openstack_requirements/requirement.py +++ b/openstack_requirements/requirement.py @@ -242,5 +242,6 @@ def check_reqs_bounds_policy(global_reqs): lower_bound.add(spec) if len(lower_bound): yield ( - f'Requirement {req.package} should not include a >= specifier' + f'Requirement {req.package} should not include a >= ' + f'specifier' ) diff --git a/openstack_requirements/tests/test_check.py b/openstack_requirements/tests/test_check.py index 09b28a581..51b02f640 100644 --- a/openstack_requirements/tests/test_check.py +++ b/openstack_requirements/tests/test_check.py @@ -44,11 +44,15 @@ def test_extras__setup_cfg(self): req_list = check.RequirementsList('test-project', project_data) req_list.process(strict=False) - self.assertIn("setup.cfg ('test' extra)", req_list.reqs_by_file) - self.assertIn("setup.cfg ('dev' extra)", req_list.reqs_by_file) + self.assertIn( + "setup.cfg ('test' extra)", req_list.optional_reqs_by_file + ) + self.assertIn( + "setup.cfg ('dev' extra)", req_list.optional_reqs_by_file + ) - test_reqs = req_list.reqs_by_file["setup.cfg ('test' extra)"] - dev_reqs = req_list.reqs_by_file["setup.cfg ('dev' extra)"] + test_reqs = req_list.optional_reqs_by_file["setup.cfg ('test' extra)"] + dev_reqs = req_list.optional_reqs_by_file["setup.cfg ('dev' extra)"] self.assertEqual(2, len(test_reqs)) self.assertIn('pytest', test_reqs) @@ -88,30 +92,33 @@ def test_extras__pyproject_toml(self): self.assertIn('pyproject.toml (dependencies)', req_list.reqs_by_file) self.assertIn( - "pyproject.toml ('security' extra)", req_list.reqs_by_file + "pyproject.toml ('security' extra)", + req_list.optional_reqs_by_file, ) self.assertIn( - "pyproject.toml ('test' dependency group)", req_list.reqs_by_file + "pyproject.toml ('test' dependency group)", + req_list.optional_reqs_by_file, ) self.assertIn( - "pyproject.toml ('typing' dependency group)", req_list.reqs_by_file + "pyproject.toml ('typing' dependency group)", + req_list.optional_reqs_by_file, ) self.assertIn( "pyproject.toml ('typing-test' dependency group)", - req_list.reqs_by_file, + req_list.optional_reqs_by_file, ) main_reqs = req_list.reqs_by_file['pyproject.toml (dependencies)'] - security_reqs = req_list.reqs_by_file[ + security_reqs = req_list.optional_reqs_by_file[ "pyproject.toml ('security' extra)" ] - test_reqs = req_list.reqs_by_file[ + test_reqs = req_list.optional_reqs_by_file[ "pyproject.toml ('test' dependency group)" ] - typing_reqs = req_list.reqs_by_file[ + typing_reqs = req_list.optional_reqs_by_file[ "pyproject.toml ('typing' dependency group)" ] - typing_test_reqs = req_list.reqs_by_file[ + typing_test_reqs = req_list.optional_reqs_by_file[ "pyproject.toml ('typing-test' dependency group)" ] @@ -143,7 +150,6 @@ def setUp(self): self._stdout_fixture = fixtures.StringStream('stdout') self.stdout = self.useFixture(self._stdout_fixture).stream - self.backports = list() self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.stdout)) self.global_reqs = check.get_global_reqs( @@ -151,6 +157,7 @@ def setUp(self): name>=1.2,!=1.4 withmarker>=1.5;python_version=='3.5' withmarker>=1.2,!=1.4;python_version=='2.7' + withwinmarker>=1.0;sys_platform!='win32' """) ) @@ -161,7 +168,6 @@ def test_match(self): check._is_requirement_in_global_reqs( req, self.global_reqs['name'], - self.backports, ) ) @@ -176,7 +182,6 @@ def test_match_with_markers(self): check._is_requirement_in_global_reqs( req, self.global_reqs['withmarker'], - self.backports, ) ) @@ -191,7 +196,6 @@ def test_match_with_local_markers(self): check._is_requirement_in_global_reqs( req, self.global_reqs['name'], - self.backports, ) ) @@ -210,22 +214,49 @@ def test_match_without_python3_markers(self): check._is_requirement_in_global_reqs( req, self.global_reqs['withmarker'], - self.backports, ) ) - def test_backport(self): - """Test a stdlib backport pacakge. + def test_match_with_windows_markers(self): + """Test a package specified with Windows markers.""" + req = requirement.parse( + textwrap.dedent(""" + withwinmarker>=1.0;sys_platform!='win32' + """) + )['withwinmarker'][0][0] + self.assertTrue( + check._is_requirement_in_global_reqs( + req, + self.global_reqs['withwinmarker'], + ) + ) - The python_version marker should be ignored for stdlib backport-type - packages. + def test_match_without_windows_markers(self): + """Test a package specified without Windows markers. + + OpenStack no longer supports Windows, so packages can drop Windows + markers even if they are present in global requirements. """ - req = requirement.parse("name;python_version<'3.9'")['name'][0][0] + req = requirement.parse( + textwrap.dedent(""" + withwinmarker>=1.0 + """) + )['withwinmarker'][0][0] self.assertTrue( check._is_requirement_in_global_reqs( req, - self.global_reqs['name'], - ['name'], + self.global_reqs['withwinmarker'], + ) + ) + + # Also test with double quotes + global_reqs = check.get_global_reqs( + 'withwinmarker>=1.0;sys_platform!="win32"' + ) + self.assertTrue( + check._is_requirement_in_global_reqs( + req, + global_reqs['withwinmarker'], ) ) @@ -239,7 +270,6 @@ def test_name_mismatch(self): check._is_requirement_in_global_reqs( req, self.global_reqs['name'], - self.backports, ) ) @@ -254,7 +284,6 @@ def test_marker_mismatch(self): check._is_requirement_in_global_reqs( req, self.global_reqs['name'], - self.backports, ) ) @@ -270,7 +299,6 @@ def test_min_mismatch(self): check._is_requirement_in_global_reqs( req, self.global_reqs['name'], - self.backports, ) ) @@ -285,7 +313,6 @@ def test_extra_exclusion(self): check._is_requirement_in_global_reqs( req, self.global_reqs['name'], - self.backports, ) ) @@ -299,7 +326,6 @@ def test_missing_exclusion(self): check._is_requirement_in_global_reqs( req, self.global_reqs['name'], - self.backports, ) ) @@ -333,7 +359,6 @@ def setUp(self): self._stdout_fixture = fixtures.StringStream('stdout') self.stdout = self.useFixture(self._stdout_fixture).stream self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.stdout)) - self.backports = dict() def test_unchanged(self): # If the line matches the value in the branch list everything @@ -345,8 +370,8 @@ def test_unchanged(self): 'name', reqs=reqs, denylist=requirement.parse(''), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) @@ -359,8 +384,8 @@ def test_denylisted(self): 'name', reqs=reqs, denylist=requirement.parse('name'), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) @@ -374,8 +399,8 @@ def test_denylisted_mismatch(self): 'name', reqs=reqs, denylist=requirement.parse('name'), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) @@ -388,8 +413,8 @@ def test_not_in_global_list(self): 'name', reqs=reqs, denylist=requirement.parse(''), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) @@ -402,8 +427,25 @@ def test_new_item_matches_global_list(self): 'name', reqs=reqs, denylist=requirement.parse(''), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, + ) + ) + + def test_new_item_matches_global_list_without_windows_marker(self): + # A project can omit the Windows marker even if it's present in the + # global list. + reqs = [r for r, line in requirement.parse('name>=1.2,!=1.4')['name']] + global_reqs = check.get_global_reqs( + "name>=1.2,!=1.4;sys_platform!='win32'" + ) + self.assertFalse( + check._validate_one( + 'name', + reqs=reqs, + denylist=requirement.parse(''), + global_reqs=global_reqs, + is_optional=False, ) ) @@ -417,8 +459,8 @@ def test_new_item_lower_min(self): 'name', reqs=reqs, denylist=requirement.parse(''), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) @@ -434,8 +476,8 @@ def test_new_item_extra_exclusion(self): 'name', reqs=reqs, denylist=requirement.parse(''), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) @@ -449,8 +491,8 @@ def test_new_item_missing_exclusion(self): 'name', reqs=reqs, denylist=requirement.parse(''), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) @@ -474,8 +516,8 @@ def test_new_item_matches_global_list_with_extra(self): 'name', reqs=reqs, denylist=requirement.parse(''), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) @@ -498,8 +540,8 @@ def test_new_item_missing_extra_line(self): 'name', reqs=reqs, denylist=requirement.parse(''), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) @@ -523,49 +565,69 @@ def test_new_item_mismatches_global_list_with_extra(self): 'name', reqs=reqs, denylist=requirement.parse(''), - backports=self.backports, global_reqs=global_reqs, + is_optional=False, ) ) - -class TestBackportPythonMarkers(testtools.TestCase): - def setUp(self): - super().setUp() - self._stdout_fixture = fixtures.StringStream('stdout') - self.stdout = self.useFixture(self._stdout_fixture).stream - self.useFixture(fixtures.MonkeyPatch('sys.stdout', self.stdout)) - - self.req = requirement.parse( - textwrap.dedent(""" - name>=1.5;python_version=='3.11' - """) - )['name'][0][0] - self.global_reqs = check.get_global_reqs( - textwrap.dedent(""" - name>=1.5;python_version=='3.10' - """) + def test_optional_not_in_global_list(self): + # If an optional package is not in the global list, that is not an + # error. + reqs = [r for r, line in requirement.parse('name>=1.2,!=1.4')['name']] + global_reqs = check.get_global_reqs('') + self.assertFalse( + check._validate_one( + 'name', + reqs=reqs, + denylist=requirement.parse(''), + global_reqs=global_reqs, + is_optional=True, + ) ) - def test_notmatching_no_backport(self): - backports = requirement.parse("") + def test_optional_matches_global_list(self): + # If an optional package is in the global list and matches, + # everything is OK. + reqs = [r for r, line in requirement.parse('name>=1.2,!=1.4')['name']] + global_reqs = check.get_global_reqs('name>=1.2,!=1.4') self.assertFalse( - check._is_requirement_in_global_reqs( - self.req, - self.global_reqs["name"], - list(backports.keys()), + check._validate_one( + 'name', + reqs=reqs, + denylist=requirement.parse(''), + global_reqs=global_reqs, + is_optional=True, ) ) - def test_notmatching_with_backport(self): - b_content = textwrap.dedent(""" - name - """) - backports = requirement.parse(b_content) + def test_optional_mismatches_global_list(self): + # If an optional package is in the global list and does not match, + # that is an error. + reqs = [ + r for r, line in requirement.parse('name>=1.2,!=1.4,!=1.5')['name'] + ] + global_reqs = check.get_global_reqs('name>=1.2,!=1.4') self.assertTrue( - check._is_requirement_in_global_reqs( - self.req, - self.global_reqs["name"], - list(backports.keys()), + check._validate_one( + 'name', + reqs=reqs, + denylist=requirement.parse(''), + global_reqs=global_reqs, + is_optional=True, + ) + ) + + def test_optional_denylisted(self): + # If the optional package is denylisted, everything is OK even if + # absent from global-requirements. + reqs = [r for r, line in requirement.parse('name>=1.2,!=1.4')['name']] + global_reqs = check.get_global_reqs('') + self.assertFalse( + check._validate_one( + 'name', + reqs=reqs, + denylist=requirement.parse('name'), + global_reqs=global_reqs, + is_optional=True, ) ) diff --git a/openstack_requirements/tests/test_check_constraints.py b/openstack_requirements/tests/test_check_constraints.py index 311944a56..3592c1dd8 100644 --- a/openstack_requirements/tests/test_check_constraints.py +++ b/openstack_requirements/tests/test_check_constraints.py @@ -14,15 +14,12 @@ import os from unittest import mock -import testscenarios import testtools from openstack_requirements.cmds import check_exists from openstack_requirements import project from openstack_requirements.tests import common -load_tests = testscenarios.load_tests_apply_scenarios - def mock_read_requirements_file(filename): if os.path.basename(filename) == 'upper-constraints.txt': diff --git a/openstack_requirements/tests/test_edit_constraint.py b/openstack_requirements/tests/test_edit_constraint.py index b65acd22a..eeb80d0d2 100644 --- a/openstack_requirements/tests/test_edit_constraint.py +++ b/openstack_requirements/tests/test_edit_constraint.py @@ -15,16 +15,12 @@ import textwrap import fixtures -import testscenarios import testtools from openstack_requirements.cmds import edit_constraint as edit from openstack_requirements import requirement -load_tests = testscenarios.load_tests_apply_scenarios - - class SmokeTest(testtools.TestCase): def test_make_url(self): stdout = io.StringIO() diff --git a/openstack_requirements/tests/test_generate.py b/openstack_requirements/tests/test_generate.py index b8568e993..515c3d98b 100644 --- a/openstack_requirements/tests/test_generate.py +++ b/openstack_requirements/tests/test_generate.py @@ -34,7 +34,7 @@ def test_freeze_smoke(self): [ py, '-c', - 'import pkgutil; [print(x) for x in pkgutil.iter_modules()]', + 'import pkgutil; [print(x) for x in pkgutil.iter_modules()]', # noqa: E501 ] ) ) diff --git a/openstack_requirements/tests/test_project.py b/openstack_requirements/tests/test_project.py index 99f3dd8b5..336829fec 100644 --- a/openstack_requirements/tests/test_project.py +++ b/openstack_requirements/tests/test_project.py @@ -15,16 +15,12 @@ import textwrap import fixtures -import testscenarios import testtools from openstack_requirements import project from openstack_requirements.tests import common -load_tests = testscenarios.load_tests_apply_scenarios - - class TestReadProject(testtools.TestCase): def test_pyproject_toml(self): root = self.useFixture(common.pep_518_fixture).root diff --git a/openstack_requirements/tests/test_requirement.py b/openstack_requirements/tests/test_requirement.py index 1de817623..14132356c 100644 --- a/openstack_requirements/tests/test_requirement.py +++ b/openstack_requirements/tests/test_requirement.py @@ -12,187 +12,158 @@ import textwrap -import testscenarios import testtools from openstack_requirements import requirement -load_tests = testscenarios.load_tests_apply_scenarios - - class TestParseRequirement(testtools.TestCase): dist_scenarios = [ ( 'package', - dict( - line='swift', - req=requirement.Requirement('swift', '', '', '', ''), - ), + 'swift', + requirement.Requirement('swift', '', '', '', ''), + False, ), ( 'specifier', - dict( - line='alembic>=0.4.1', - req=requirement.Requirement('alembic', '', '>=0.4.1', '', ''), - ), + 'alembic>=0.4.1', + requirement.Requirement('alembic', '', '>=0.4.1', '', ''), + False, ), ( 'specifiers', - dict( - line='alembic>=0.4.1,!=1.1.8', - req=requirement.Requirement( - 'alembic', '', '!=1.1.8,>=0.4.1', '', '' - ), - ), + 'alembic>=0.4.1,!=1.1.8', + requirement.Requirement('alembic', '', '!=1.1.8,>=0.4.1', '', ''), + False, ), ( 'comment-only', - dict( - line='# foo', - req=requirement.Requirement('', '', '', '', '# foo'), - ), + '# foo', + requirement.Requirement('', '', '', '', '# foo'), + False, ), ( 'comment', - dict( - line='Pint>=0.5 # BSD', - req=requirement.Requirement('Pint', '', '>=0.5', '', '# BSD'), - ), + 'Pint>=0.5 # BSD', + requirement.Requirement('Pint', '', '>=0.5', '', '# BSD'), + False, ), ( 'comment-with-semicolon', - dict( - line='Pint>=0.5 # BSD;fred', - req=requirement.Requirement( - 'Pint', '', '>=0.5', '', '# BSD;fred' - ), - ), + 'Pint>=0.5 # BSD;fred', + requirement.Requirement('Pint', '', '>=0.5', '', '# BSD;fred'), + False, ), ( 'case', - dict( - line='Babel>=1.3', - req=requirement.Requirement('Babel', '', '>=1.3', '', ''), - ), + 'Babel>=1.3', + requirement.Requirement('Babel', '', '>=1.3', '', ''), + False, ), ( 'markers', - dict( - line="pywin32;sys_platform=='win32'", - req=requirement.Requirement( - 'pywin32', '', '', "sys_platform=='win32'", '' - ), + "pywin32;sys_platform=='win32'", + requirement.Requirement( + 'pywin32', '', '', "sys_platform=='win32'", '' ), + False, ), ( 'markers-with-comment', - dict( - line="Sphinx<=1.2; python_version=='2.7'# Sadface", - req=requirement.Requirement( - 'Sphinx', '', '<=1.2', "python_version=='2.7'", '# Sadface' - ), + "Sphinx<=1.2; python_version=='2.7'# Sadface", + requirement.Requirement( + 'Sphinx', '', '<=1.2', "python_version=='2.7'", '# Sadface' ), + False, ), ] url_scenarios = [ ( 'url', - dict( - line='file:///path/to/thing#egg=thing', - req=requirement.Requirement( - 'thing', 'file:///path/to/thing', '', '', '' - ), - permit_urls=True, + 'file:///path/to/thing#egg=thing', + requirement.Requirement( + 'thing', 'file:///path/to/thing', '', '', '' ), + True, ), ( 'oslo-url', - dict( - line='file:///path/to/oslo.thing#egg=oslo.thing', - req=requirement.Requirement( - 'oslo.thing', 'file:///path/to/oslo.thing', '', '', '' - ), - permit_urls=True, + 'file:///path/to/oslo.thing#egg=oslo.thing', + requirement.Requirement( + 'oslo.thing', 'file:///path/to/oslo.thing', '', '', '' ), + True, ), ( 'url-comment', - dict( - line='file:///path/to/thing#egg=thing # http://altpath#egg=boo', - req=requirement.Requirement( - 'thing', - 'file:///path/to/thing', - '', - '', - '# http://altpath#egg=boo', - ), - permit_urls=True, + 'file:///path/to/thing#egg=thing # http://altpath#egg=boo', + requirement.Requirement( + 'thing', + 'file:///path/to/thing', + '', + '', + '# http://altpath#egg=boo', ), + True, ), ( 'editable', - dict( - line='-e file:///path/to/bar#egg=bar', - req=requirement.Requirement( - 'bar', '-e file:///path/to/bar', '', '', '' - ), - permit_urls=True, + '-e file:///path/to/bar#egg=bar', + requirement.Requirement( + 'bar', '-e file:///path/to/bar', '', '', '' ), + True, ), ( 'editable_vcs_git', - dict( - line='-e git+http://github.com/path/to/oslo.bar#egg=oslo.bar', - req=requirement.Requirement( - 'oslo.bar', - '-e git+http://github.com/path/to/oslo.bar', - '', - '', - '', - ), - permit_urls=True, + '-e git+http://github.com/path/to/oslo.bar#egg=oslo.bar', + requirement.Requirement( + 'oslo.bar', + '-e git+http://github.com/path/to/oslo.bar', + '', + '', + '', ), + True, ), ( 'editable_vcs_git_ssh', - dict( - line='-e git+ssh://github.com/path/to/oslo.bar#egg=oslo.bar', - req=requirement.Requirement( - 'oslo.bar', - '-e git+ssh://github.com/path/to/oslo.bar', - '', - '', - '', - ), - permit_urls=True, + '-e git+ssh://github.com/path/to/oslo.bar#egg=oslo.bar', + requirement.Requirement( + 'oslo.bar', + '-e git+ssh://github.com/path/to/oslo.bar', + '', + '', + '', ), + True, ), ] scenarios = dist_scenarios + url_scenarios def test_parse(self): - parsed = requirement.parse_line( - self.line, permit_urls=getattr(self, 'permit_urls', False) - ) - self.assertEqual(self.req, parsed) + for name, line, req, permit_urls in self.scenarios: + with self.subTest(name): + parsed = requirement.parse_line(line, permit_urls=permit_urls) + self.assertEqual(req, parsed) class TestParseRequirementFailures(testtools.TestCase): scenarios = [ ( 'url', - dict( - line='http://tarballs.openstack.org/oslo.config/' - 'oslo.config-1.2.0a3.tar.gz#egg=oslo.config' - ), + 'http://tarballs.openstack.org/oslo.config/oslo.config-1.2.0a3.tar.gz#egg=oslo.config', ), - ('-e', dict(line='-e git+https://foo.com#egg=foo')), - ('-f', dict(line='-f http://tarballs.openstack.org/')), + ('-e', '-e git+https://foo.com#egg=foo'), + ('-f', '-f http://tarballs.openstack.org/'), ] def test_does_not_parse(self): - self.assertRaises(ValueError, requirement.parse_line, self.line) + for name, line in self.scenarios: + with self.subTest(name): + with self.assertRaises(ValueError): + requirement.parse_line(line) class TestToContent(testtools.TestCase): diff --git a/playbooks/files/project-requirements-change.py b/playbooks/files/project-requirements-change.py index 1baf73733..f063515d5 100755 --- a/playbooks/files/project-requirements-change.py +++ b/playbooks/files/project-requirements-change.py @@ -118,13 +118,6 @@ def main(): with open(os.path.join(reqdir, 'denylist.txt')) as fh: denylist = requirement.parse(fh.read()) - backports_file = os.path.join(reqdir, 'backports.txt') - if os.path.exists(backports_file): - with open(backports_file) as fh: - backports = requirement.parse(fh.read()) - else: - backports = {} - cwd = os.getcwd() # Verify that pyproject.toml is present and contains the required @@ -153,7 +146,6 @@ def main(): head_reqs, denylist, global_reqs, - list(backports.keys()), ) # report the results diff --git a/pyproject.toml b/pyproject.toml index 44691de36..5df4dd082 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "UP"] +select = ["E", "F", "UP"] # [tool.ruff.lint.per-file-ignores] # "openstack/tests/*" = ["S"] diff --git a/requirements.txt b/requirements.txt index 1ed6c384f..dd27509d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ +# NOTE: These are requirements for the requirements project only +# See global-requirements for the actual requirements list fixtures>=3.0.0 # Apache-2.0/BSD packaging!=20.5,!=20.6,!=20.7,>=16.5 # Apache-2.0 requests>=2.14.2 # Apache-2.0 PyYAML>=3.12 # MIT beagle>=0.2.1 # Apache-2.0 setuptools!=24.0.0,!=34.0.0,!=34.0.1,!=34.0.2,!=34.0.3,!=34.1.0,!=34.1.1,!=34.2.0,!=34.3.0,!=34.3.1,!=34.3.2,!=36.2.0,>=21.0.0 # PSF/ZPL -tomli;python_version<'3.11' # MIT diff --git a/test-requirements.txt b/test-requirements.txt index 69ff89422..c710fc3e1 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,5 @@ # NOTE: These are requirements for testing the requirements project only # See global-requirements for the actual requirements list stestr>=1.0.0 # Apache-2.0 -testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT virtualenv>=14.0.6 # MIT diff --git a/tox.ini b/tox.ini index e335a8c07..827bfe34a 100644 --- a/tox.ini +++ b/tox.ini @@ -62,14 +62,14 @@ commands = validate-projects {toxinidir}/projects.txt description = Perform linting skip_install = true deps = - ruff~=0.13.0 # MIT - hacking~=7.0 # Apache-2.0 + ruff~=0.16.0 # MIT + hacking~=8.0 # Apache-2.0 bashate~=2.1 # Apache-2.0 allowlist_externals = bash commands = - ruff check --fix --unsafe-fixes - ruff format + ruff check --fix --unsafe-fixes --exit-non-zero-on-fix + ruff format --exit-non-zero-on-format flake8 bash -c "find {toxinidir}/tools \ -type f \ diff --git a/upper-constraints.txt b/upper-constraints.txt index 757449101..190d18649 100644 --- a/upper-constraints.txt +++ b/upper-constraints.txt @@ -8,15 +8,15 @@ voluptuous===0.16.0 chardet===6.0.0.post1 enum-compat===0.0.3 -netmiko===4.7.0 +netmiko===4.8.0 sshtunnel===0.4.0 PasteDeploy===3.1.0 Routes===2.5.1 rtslib-fb===2.2.4 oslo.limit===2.12.0 restructuredtext_lint===2.0.2 -yarl===1.24.5 -tzdata===2026.3 +yarl===1.25.1 +tzdata===2026.4 smmap===5.0.3 confget===5.1.2 XStatic-Angular-Bootstrap===2.5.0.1 @@ -24,34 +24,34 @@ WebOb===1.8.11 sphinxcontrib-actdiag===3.0.0 pecan===1.8.0 os-api-ref===3.2.0 -python-ldap===3.4.7 +python-ldap===3.4.8 oslo.concurrency===7.6.1 -websocket-client===1.9.0 +websocket-client===1.9.2 osprofiler===4.4.0 os-resource-classes===1.1.0 mypy_extensions===1.1.0 tabulate===0.10.0 python-ironic-inspector-client===5.4.0 -lxml===6.1.1 +lxml===6.1.3 vintage===0.4.1 rst2txt===1.1.0 -setproctitle===1.3.7 +setproctitle===1.3.8 pytest===9.1.1 a2wsgi===1.10.10 -python-slugify===8.0.4 +python-slugify===9.1.2 cursive===0.3.1 oslo.service===4.8.0 django-appconf===1.2.0 -ntc_templates===9.2.0 +ntc_templates===9.3.0 sphinxcontrib-nwdiag===2.0.0 rbd-iscsi-client===0.1.8 alabaster===1.0.0 -multidict===6.7.1 -pbr===7.0.3 +multidict===6.9.1 +pbr===7.1.3 munch===4.0.0 waiting===1.5.0 attrs===26.1.0 -jwcrypto===1.5.8 +jwcrypto===1.6.1 Pint===0.25.3 oslo.i18n===6.9.0 jsonpath-rw-ext===1.2.2 @@ -59,140 +59,151 @@ python-mistralclient===6.3.0 oslo.context===6.5.0 rcssmin===1.2.2 pycadf===4.1.0 -grpcio===1.83.0 +grpcio===1.84.0 sniffio===1.3.1 fixtures===4.3.2 neutron-lib===5.0.1 XStatic-FileSaver===1.3.2.1 jaraco.functools===4.6.0 +annotated-doc===0.0.5 +jaraco.text===4.3.0 oslo.metrics===0.17.0 storage-interfaces===1.0.5 -pydantic===2.13.4 +pydantic===2.13.5 pystache===0.6.8 XStatic-Font-Awesome===6.2.1.2 aiohttp===3.14.3 waitress===3.0.2 -os-refresh-config===14.0.1 -pysnmp===7.1.28 -Mako===1.4.1 +os-refresh-config===14.1.0 +pysnmp===7.1.30 +Mako===1.4.3 sphinxcontrib-htmlhelp===2.1.0 XStatic-jQuery===3.7.1.1 sphinx-copybutton===0.5.2 beartype===0.22.9 +jaraco.collections===5.2.1 ddt===1.7.2 pyserial===3.5 -moto===5.2.2 +moto===5.2.3 infi.dtypes.wwn===0.2.0 -awscrt===0.36.2 +awscrt===0.37.0 pcre2===0.7.1 python-freezerclient===6.5.0 -py-pure-client===1.93.0 +py-pure-client===1.95.0 krest===1.3.8 -psycopg2===2.9.12 +psycopg2===2.9.13 networkx===3.6.1 cheroot===11.1.2 XStatic-Angular===1.8.2.3 zuul-sphinx===0.8.1 ply===3.11 -google-api-core===2.34.0 +google-api-core===2.40.0 requests-toolbelt===1.0.0 -simplejson===4.1.1 +simplejson===4.2.0 python-swiftclient===4.11.0 -pyOpenSSL===24.2.1 +shellingham===1.5.4 +pyOpenSSL===26.4.0 typing-inspection===0.4.4 monasca-common===3.8.0 hyperframe===6.1.0 -zeroconf===0.150.0 +zeroconf===0.151.5 scipy===1.17.1 -opentelemetry-exporter-otlp===1.44.0 +opentelemetry-exporter-otlp===1.45.0 +python-gnupg===0.5.7 rsd-lib===1.2.0 XStatic-Jasmine===2.4.1.3 -googleapis-common-protos===1.75.1 +googleapis-common-protos===1.75.5 python-glanceclient===4.13.0 prometheus_client===0.26.0 jaraco.classes===3.4.0 +typer-slim===0.24.0 debtcollector===3.1.0 -responses===0.26.2 +responses===0.26.3 prompt_toolkit===3.0.53 croniter===6.2.4 horizon===27.0.0 octavia-lib===3.12.0 python-watcherclient===4.11.0 -MarkupSafe===3.0.3 +MarkupSafe===3.0.4 +cloudpickle===3.1.2 doc8===2.0.0 -pymongo===4.17.0 +pymongo===4.18.2 python-cloudkittyclient===6.2.0 -soupsieve===2.9.2 +soupsieve===2.10 sqlparse===0.6.0 oslotest===6.1.1 jsonpointer===3.1.1 defusedxml===0.7.1 -opentelemetry-sdk===1.44.0 +opentelemetry-sdk===1.45.0 netaddr===1.3.0 pyghmi===1.6.19 sphinxcontrib-blockdiag===3.0.0 aiosqlite===0.22.1 priority===2.0.0 gnocchiclient===7.2.0 -wcwidth===0.8.2 +wcwidth===0.9.1 sphinxcontrib.datatemplates===0.11.0 jsonpath-rw===1.4.0 prettytable===3.18.0 vine===5.1.0 pathspec===1.1.1 -taskflow===6.4.0 +taskflow===6.5.0 arrow===1.4.0 semantic-version===2.10.0 -ConfigArgParse===1.7.5 +ConfigArgParse===1.8.0 async-timeout===5.0.1 virtualbmc===3.3.0 -SQLAlchemy===2.0.52 +SQLAlchemy===2.0.54 pyroute2===0.8.1 -google-auth===2.56.3 +google-auth===2.59.1 kazoo===2.11.0 -pyspnego===0.12.1 +pyspnego===0.12.3 trio-websocket===0.12.2 XStatic-roboto-fontface===0.8.0.1 -pyudev===0.24.4 +pyudev===0.24.5 eventlet===0.41.2 openstack-doc-tools===4.0.3 oslo.messaging===18.3.0 jira===3.10.5 -PyJWT===2.13.0 +PyJWT===2.15.1 typing_extensions===4.16.0 zVMCloudConnector===1.6.3 -paramiko===4.0.0 +paramiko===5.0.0 ifaddr===0.2.0 reno===4.1.0 ncclient===0.7.1 -imagesize===2.0.0 +imagesize===2.0.1 pydot===4.0.1 -urllib3===2.7.0 +urllib3===2.8.0 graphviz===0.21 -PyKMIP===0.10.0 +portend===3.2.1 +PyKMIP===0.11.0 python-observabilityclient===1.4.0 whereto===0.6.0 -networking-generic-switch===10.1.0 -pywbem===1.9.0 +networking-generic-switch===10.2.0 +pywbem===1.9.1 python-subunit===1.4.6 +tornado===6.5.10 pycparser===3.0 mock===5.2.0 PyYAML===6.0.3 beautifulsoup4===4.15.0 -ovs===3.7.1 -cryptography===43.0.3 +ovs===4.0.0 +cryptography===50.0.2 httpcore===1.0.9 URLObject===3.0.0 -psycopg2-binary===2.9.12 +psycopg2-binary===2.9.13 glance_store===5.7.0 -openstack-release-test===8.5.0 +openstack-release-test===8.6.0 +pycryptodomex===3.23.0 requests-mock===1.12.1 -os-apply-config===14.0.1 -gunicorn===26.0.0 +os-apply-config===14.1.0 +xxhash===4.0.1 +gunicorn===26.2.0 storpool===7.3.0 textfsm===2.1.0 python-3parclient===4.4 -libvirt-python===12.6.0 +libvirt-python===12.8.0 python-zunclient===5.5.0 tzlocal===5.4.4 sysv_ipc===1.2.0 @@ -207,7 +218,7 @@ os-client-config===2.3.0 XStatic-Angular-Gettext===2.4.1.1 Deprecated===1.3.1 h11===0.16.0 -Pygments===2.20.0 +Pygments===2.21.0 XStatic-Hogan===2.0.0.5 api_object_schema===2.0.0 XStatic-objectpath===1.2.1.1 @@ -219,10 +230,11 @@ Jinja2===3.1.6 XStatic-Bootstrap-SCSS===3.4.1.1 pyzabbix===1.3.1 ptyprocess===0.7.0 -amqp===5.3.1 +amqp===5.4.0 ruamel.yaml===0.19.1 websockify===0.13.0 -gssapi===1.11.1 +CherryPy===18.10.0 +gssapi===1.12.0 XStatic-JQuery.quicksearch===2.0.3.3 pyasn1_modules===0.4.2 mpmath===1.3.0 @@ -233,8 +245,9 @@ sphinx-feature-classification===2.1.0 XStatic-JQuery-Migrate===3.3.2.2 pytest-html===4.2.0 appdirs===1.4.4 -google-auth-httplib2===0.4.1 +google-auth-httplib2===0.4.4 daiquiri===3.4.0 +apache-libcloud===3.9.1 influxdb===5.3.2 funcparserlib===2.0.0a0 passlib===1.7.4 @@ -243,53 +256,56 @@ os-brick===7.1.1 valkey===6.1.1 scp===0.16.1 lark===1.3.1 +opentelemetry-exporter-prometheus===0.66b0 python-zaqarclient===4.7.0 ldappool===3.0.0 hpack===4.2.0 -joblib===1.5.3 +joblib===1.6.0 roman-numerals===4.1.0 -google-api-python-client===2.198.0 +google-api-python-client===2.201.0 castellan===5.8.0 oslo.versionedobjects===3.12.0 enmerkar===0.7.1 +typer===0.27.2 webcolors===25.10.0 aodhclient===3.12.0 autobahn===26.7.1 +truststore===0.10.4 SQLAlchemy-Utils===0.42.1 retryz===0.1.9 pluggy===1.6.0 -coverage===7.15.4 +coverage===7.16.2 pyee===13.0.1 freezegun===1.5.5 mdurl===0.1.2 toml===0.10.2 -pycdlib===1.20.0 +pycdlib===1.21.0 pyperclip===1.11.0 cassandra-driver===3.30.1 XStatic-Angular-Schema-Form===0.8.13.1 -opentelemetry-exporter-otlp-proto-http===1.44.0 -gabbi===4.2.0 +opentelemetry-exporter-otlp-proto-http===1.45.0 +gabbi===4.2.3 nwdiag===3.0.0 XStatic-bootswatch===3.3.7.1 annotated-types===0.8.0 pytest-xdist===3.8.0 XStatic-JS-Yaml===3.13.1.2 XStatic-term.js===0.0.7.1 -oslo.log===8.3.1 -nodeenv===1.10.0 +oslo.log===8.3.2 +nodeenv===1.11.0 gossip===2.5.0 suds-community===1.2.0 -os_vif===5.2.1 +os_vif===5.3.0 qrcode===8.2 oslo.middleware===8.2.0 XStatic-mdi===1.6.50.3 -pydantic_core===2.46.4 +pydantic_core===2.46.5 uritemplate===4.2.0 docutils===0.21.2 -threadpoolctl===3.6.0 +threadpoolctl===3.7.0 os-ken===4.2.2 -ujson===5.13.0 -selenium===4.47.0 +ujson===6.0.0 +selenium===4.50.0 pytest-subtests===0.15.0 mistral-lib===3.6.0 dogtag-pki===11.2.1 @@ -298,11 +314,11 @@ dfs_sdk===1.2.27 sphinxcontrib-seqdiag===3.0.0 os-win===5.9.0 capacity===1.3.14 -playwright===1.62.0 +playwright===1.63.0 markdown-it-py===4.2.0 retrying===1.4.2 -python-discovery===1.5.2 -platformdirs===4.11.3 +python-discovery===1.6.1 +platformdirs===4.12.2 pydotplus===2.0.2 boto3===1.35.99 jeepney===0.9.0 @@ -311,7 +327,7 @@ pillow===12.3.0 infoblox-client===0.6.2 oslo.serialization===5.11.0 warlock===2.1.0 -exabgp===5.0.10 +exabgp===5.0.13 aiomysql===0.3.2 sphinxcontrib-httpdomain===2.0.0 metalsmith===2.5.0 @@ -322,7 +338,7 @@ oslo.vmware===4.10.1 autopage===0.6.0 gitdb===4.0.12 python-monascaclient===2.8.0 -opentelemetry-api===1.44.0 +opentelemetry-api===1.45.0 frozenlist===1.8.0 automaton===3.5.0 os-service-types===1.9.0 @@ -330,17 +346,17 @@ keyring===25.7.0 elementpath===4.8.0 wsgi_intercept===1.13.1 jsonschema-specifications===2025.9.1 -testscenarios===0.6.2 +testscenarios===0.7.0 sphinxcontrib-pecanwsme===0.11.0 sadisplay===0.4.9 enum34===1.1.10 infinisdk===289.1.4 rich-argparse===1.8.0 packaging===26.3 -opentelemetry-exporter-otlp-proto-grpc===1.44.0 +opentelemetry-exporter-otlp-proto-grpc===1.45.0 psutil===7.2.2 txaio===26.6.1 -elasticsearch===9.5.0 +elasticsearch===9.5.1 asgiref===3.12.1 XStatic-JQuery.TableSorter===2.14.5.3 pifpaf===3.4.0 @@ -361,19 +377,19 @@ SecretStorage===3.5.0 XStatic-Rickshaw===1.5.1.3 iso8601===2.1.0 tooz===9.1.0 -idna===3.18 +idna===3.20 Hypercorn===0.18.0 yamlloader===1.6.0 -protobuf===7.35.1 -sushy===5.13.0 +protobuf===7.36.2 +sushy===5.14.0 python-neutronclient===14.0.0 pika===1.4.4 oslo.cache===4.3.0 WebTest===3.0.7 -os-collect-config===14.0.1 -edgegrid-python===2.0.7 +os-collect-config===14.1.0 +edgegrid-python===2.0.8 python-octaviaclient===3.15.0 -pysaml2===7.5.4 +pysaml2===7.5.5 requests-oauthlib===2.0.0 oslo.reports===3.9.0 bitmath===2.1.1 @@ -385,8 +401,9 @@ PyNaCl===1.6.2 osc-lib===4.7.0 more-itertools===11.1.0 seqdiag===3.0.0 +zc.lockfile===4.0 numpy===2.4.6 -msgpack===1.2.1 +msgpack===1.2.3 Sphinx===9.0.4 oslo.config===10.7.0 openstackdocstheme===3.6.0 @@ -395,27 +412,29 @@ rpds-py===2026.6.3 zake===0.2.2 flux===1.4.0 flexparser===0.4 -krb5===0.9.0 -PyMySQL===1.2.0 +krb5===0.10.0 +PyMySQL===1.2.3 uhashring===2.5 kubernetes===36.0.3 httplib2===0.32.0 +opentelemetry-exporter-otlp-common===0.66b0 betamax===0.9.0 construct===2.10.70 pytest-metadata===3.1.1 pyparsing===3.3.2 geomet===1.1.0 -opentelemetry-exporter-otlp-proto-common===1.44.0 +opentelemetry-exporter-otlp-proto-common===1.45.0 distlib===0.4.3 -ast_serialize===0.8.0 +ast_serialize===0.12.0 dogpile.cache===1.5.0 python-barbicanclient===7.6.0 salt===3008.0 -opentelemetry-semantic-conventions===0.65b0 +opentelemetry-semantic-conventions===0.66b0 blinker===1.9.0 WSME===0.13.0 oslo.upgradecheck===2.8.0 sherlock===0.4.1 +tempora===5.13.0 stevedore===5.9.1 botocore===1.35.99 xmltodict===1.0.4 @@ -424,12 +443,12 @@ oslo.rootwrap===7.10.0 Django===5.2.17 pexpect===4.9.0 elastic-transport===9.4.2 -cmd2===4.2.0 -python-json-logger===4.1.0 +cmd2===4.2.4 +python-json-logger===4.2.0 redis===8.1.0 jmespath===1.1.0 -cbor2===6.1.4 -click===8.4.2 +cbor2===6.1.5 +click===8.5.0 XStatic-smart-table===1.4.13.3 kuryr-lib===3.5.0 jsonpatch===1.33 @@ -440,37 +459,40 @@ cotyledon===2.2.0 xattr===1.3.0 systemd-python===235 python-memcached===1.62 -openstacksdk===4.20.0 +openstacksdk===4.21.0 infi.dtypes.nqn===0.1.0 +looseversion===1.3.0 six===1.17.0 h2===4.4.1 -dulwich===1.2.12 +dulwich===1.2.15 sentinels===1.1.1 kombu===5.6.2 distro===1.9.0 zstd===1.5.7.2 yaql===3.2.0 -durationpy===0.10 +durationpy===0.11 requestsexceptions===1.4.0 testresources===2.1.2 -falcon===4.3.1 +falcon===4.4.0 tomlkit===0.15.1 +opentelemetry-exporter-http-transport===0.66b0 etcd3gw===2.7.0 +timelib===0.3.0 Flask-RESTful===0.3.10 -GitPython===3.1.59 +GitPython===3.2.0 python-ironicclient===6.3.0 babel===2.18.0 XStatic===1.0.3 XStatic-Angular-FileUpload===12.2.13.2 -python-openstackclient===10.3.0 -pyzmq===27.1.0 +python-openstackclient===10.4.0 +pyzmq===27.2.0 oslo.db===18.1.1 simplegeneric===0.8.1 yappi===1.7.6 mbstrdecoder===1.1.5 wsproto===1.3.2 pymemcache===4.0.0 -wrapt===2.3.0 +wrapt===2.5.0 PySocks===1.7.1 oslo.privsep===3.12.0 sphinxcontrib-apidoc===0.6.0 @@ -490,37 +512,37 @@ boto===2.49.0 hyperlink===21.0.0 mitba===1.1.1 python-masakariclient===8.9.0 -Werkzeug===3.1.8 +Werkzeug===3.1.9 outcome===1.3.0.post0 APScheduler===3.11.3 xmlschema===2.5.1 python-troveclient===8.11.0 -hpe-storage-flowkit-py===0.9.5 +hpe-storage-flowkit-py===1.0.1 cachez===0.1.2 XStatic-Bootstrap-Datepicker===1.4.0.1 netifaces===0.11.0 -propcache===0.5.2 -cachetools===7.1.7 +propcache===0.5.4 +cachetools===7.2.0 flexcache===0.3 sphinxcontrib-qthelp===2.0.0 -keystoneauth1===5.17.0 +keystoneauth1===5.18.0 statsd===4.0.1 -proto-plus===1.28.3 +proto-plus===1.29.0 python-keystoneclient===6.0.0 -diskimage-builder===3.42.0 +diskimage-builder===3.43.0 py-consul===1.7.1 heat-translator===3.5.0 python-magnumclient===5.0.0 docker===7.2.0 repoze-lru===0.8 storops===1.2.12 -anyio===4.14.2 +anyio===4.15.1 aiosignal===1.4.0 XStatic-Angular-lrdragndrop===1.0.2.7 ovsdbapp===2.19.0 aniso8601===10.0.1 rjsmin===1.2.5 -icalendar===7.2.2 +icalendar===7.3.0 configparser===7.2.0 decorator===5.3.1 DateTimeRange===2.3.2 @@ -530,36 +552,35 @@ futurist===3.5.0 jsonschema===4.26.0 sphinxcontrib-devhelp===2.0.0 python-blazarclient===4.6.0 -alembic===1.19.1 +alembic===1.20.0 execnet===2.1.2 sphinxcontrib-programoutput===0.20 storpool.spopenstack===3.2.0 dnspython===2.8.0 -oauthlib===3.3.1 +oauthlib===4.0.0 zipp===4.1.0 -greenlet===3.5.5 +greenlet===3.5.6 XStatic-Angular-Vis===4.16.0.1 iniconfig===2.3.0 referencing===0.37.0 -confluent-kafka===2.15.0 +confluent-kafka===2.15.1 backports.tarfile===1.2.0 -narwhals===2.24.0 -xvfbwrapper===0.2.29 +narwhals===2.26.0 +xvfbwrapper===0.2.34 influxdb-client===1.50.0 -tosca-parser===2.14.0 +tosca-parser===2.15.0 python-consul2===0.1.5 -charset-normalizer===3.5.0 +charset-normalizer===3.5.2 Flask===3.1.3 httpx===0.28.1 -sqlalchemy-filters===0.13.0 sphinxcontrib-runcmd===0.2.0 confspirator===0.3.0 fasteners===0.20 -importlib_metadata===9.0.0 +importlib_metadata===9.0.1 sortedcontainers===2.4.0 microversion_parse===2.1.0 -python-linstor===1.29.0 -filelock===3.32.3 +python-linstor===1.29.1 +filelock===4.0.9 python-tackerclient===2.6.0 python-heatclient===5.3.0 oslo.utils===10.2.0 @@ -568,14 +589,14 @@ itsdangerous===2.2.0 XStatic-jquery-ui===1.13.0.2 monasca-statsd===2.7.0 python-dateutil===2.9.0.post0 -virtualenv===21.7.4 +virtualenv===21.14.5 colorama===0.4.6 confetti===2.5.3 ironic-lib===7.0.0 aiohappyeyeballs===2.7.1 -pytz===2026.3.post1 -opentelemetry-proto===1.44.0 +pytz===2026.4 +opentelemetry-proto===1.45.0 XStatic-D3===3.5.17.1 actdiag===3.0.0 sphinxcontrib-applehelp===2.0.0 -scikit-learn===1.9.0 +scikit-learn===1.9.1