diff --git a/global-requirements.txt b/global-requirements.txt index a0913e3c8..bc1e8f5fb 100644 --- a/global-requirements.txt +++ b/global-requirements.txt @@ -72,6 +72,7 @@ grpcio # Apache-2.0 gunicorn # MIT httplib2 # MIT httpx # BSD +Hypercorn # MIT hvac # Apache-2.0 icalendar # BSD # Do not make importlib-metadata conditional on Python version: we depend on diff --git a/openstack_requirements/check.py b/openstack_requirements/check.py index 7b6711b24..69e417e5e 100644 --- a/openstack_requirements/check.py +++ b/openstack_requirements/check.py @@ -129,7 +129,6 @@ def _is_requirement_in_global_reqs( local_req, global_reqs, backports, - allow_3_only=False, ): req_exclusions = _get_exclusions(local_req) for global_req in global_reqs: @@ -138,10 +137,10 @@ 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 spefied in only one of + # 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 allow_3_only and matching and aname == 'markers': + if matching and aname == 'markers': if not local_req_val and PY3_GLOBAL_SPECIFIER_RE.match( global_req_val ): @@ -250,7 +249,6 @@ def _validate_one( denylist, global_reqs, backports, - allow_3_only=False, ): """Returns True if there is a failure.""" @@ -276,7 +274,6 @@ def _validate_one( req, global_reqs[name], backports, - allow_3_only, ): return True @@ -289,22 +286,8 @@ def _validate_one( return True for extra, count in counts.items(): - # Make sure the number of entries matches. If allow_3_only, then we - # just need to make sure we have at least the number of entries for - # supported Python 3 versions. + # Make sure the number of entries matches. if count != len(global_reqs[name]): - if allow_3_only and count >= len( - _get_python3_reqs(global_reqs[name]) - ): - print( - "WARNING (probably OK for Ussuri and later): " - "Package '{}{}' is only tracking python 3 " - "requirements".format( - name, (f'[{extra}]') if extra else '' - ) - ) - continue - print( "ERROR: Package '{}{}' requirement does not match " "number of lines ({}) in " @@ -324,7 +307,6 @@ def validate( denylist, global_reqs, backports, - allow_3_only=False, ): failed = False # iterate through the changing entries and see if they match the global @@ -339,7 +321,6 @@ def validate( denylist, global_reqs, backports, - allow_3_only, ) or failed ) diff --git a/openstack_requirements/project.py b/openstack_requirements/project.py index a83035f9f..4f8eafcd2 100644 --- a/openstack_requirements/project.py +++ b/openstack_requirements/project.py @@ -123,6 +123,35 @@ def _read_setup_cfg_extras(root: str) -> dict[str, list[str]] | None: return result +def verify_pyproject_toml(root: str) -> bool: + data = _read_pyproject_toml(root) + + if data is None: + print('Missing pyproject.toml file', file=sys.stderr) + return False + + if 'build-system' not in data: + print("pyproject.toml is missing 'build-system' table", file=sys.stderr) + return False + + 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}", + file=sys.stderr, + ) + return False + + if 'project' not in data: + print( + "pyproject.toml is missing 'project' table. This is not currently " + "an error but may be in the future", + file=sys.stderr, + ) + + return True + + class Project(TypedDict): # The root directory path root: str diff --git a/openstack_requirements/tests/test_check.py b/openstack_requirements/tests/test_check.py index d74b4925e..65b5d8848 100644 --- a/openstack_requirements/tests/test_check.py +++ b/openstack_requirements/tests/test_check.py @@ -113,7 +113,6 @@ def test_match_with_local_markers(self): req, self.global_reqs['name'], self.backports, - allow_3_only=True, ) ) @@ -133,7 +132,6 @@ def test_match_without_python3_markers(self): req, self.global_reqs['withmarker'], self.backports, - allow_3_only=True, ) ) @@ -451,110 +449,6 @@ def test_new_item_mismatches_global_list_with_extra(self): ) ) - def test_new_item_matches_py3_allowed_no_version(self): - # If the global list has multiple entries for an item but the branch - # allows python 3 only, then only the py3 entries need to match. - # Requirements without a python_version marker should always be used. - r_content = textwrap.dedent(""" - name>=1.5;python_version=='3.5' - other-name - """) - reqs = [r for r, line in requirement.parse(r_content)['name']] - global_reqs = check.get_global_reqs( - textwrap.dedent(""" - name>=1.5;python_version=='3.5' - name>=1.2,!=1.4;python_version=='2.6' - other-name - """) - ) - self.assertFalse( - check._validate_one( - 'name', - reqs=reqs, - denylist=requirement.parse(''), - backports=self.backports, - global_reqs=global_reqs, - allow_3_only=True, - ) - ) - - def test_new_item_matches_py3_allowed(self): - # If the global list has multiple entries for an item but the branch - # allows python 3 only, then only the py3 entries need to match. - # Requirements without a python_version marker should always be used. - r_content = textwrap.dedent(""" - name>=1.5 - other-name - """) - reqs = [r for r, line in requirement.parse(r_content)['name']] - global_reqs = check.get_global_reqs( - textwrap.dedent(""" - name>=1.5;python_version>='3.5' - name>=1.2,!=1.4;python_version=='2.6' - other-name - """) - ) - self.assertFalse( - check._validate_one( - 'name', - reqs=reqs, - denylist=requirement.parse(''), - backports=self.backports, - global_reqs=global_reqs, - allow_3_only=True, - ) - ) - - def test_new_item_matches_py3_allowed_with_py2(self): - # If the global list has multiple entries for an item but the branch - # allows python 3 only, then only the py3 entries need to match. - # It should continue to pass with py2 entries though. - r_content = textwrap.dedent(""" - name>=1.5;python_version=='3.5' - name>=1.2,!=1.4;python_version=='2.6' - """) - reqs = [r for r, line in requirement.parse(r_content)['name']] - global_reqs = check.get_global_reqs( - textwrap.dedent(""" - name>=1.5;python_version=='3.5' - name>=1.2,!=1.4;python_version=='2.6' - """) - ) - self.assertFalse( - check._validate_one( - 'name', - reqs=reqs, - denylist=requirement.parse(''), - backports=self.backports, - global_reqs=global_reqs, - allow_3_only=True, - ) - ) - - def test_new_item_matches_py3_allowed_no_py2(self): - # If the global list has multiple entries for an item but the branch - # allows python 3 only, then only the py3 entries need to match. - r_content = textwrap.dedent(""" - name>=1.5;python_version=='3.5' - """) - reqs = [r for r, line in requirement.parse(r_content)['name']] - global_reqs = check.get_global_reqs( - textwrap.dedent(""" - name>=1.5;python_version=='3.5' - name>=1.2,!=1.4;python_version=='2.6' - """) - ) - self.assertFalse( - check._validate_one( - 'name', - reqs=reqs, - denylist=requirement.parse(''), - backports=self.backports, - global_reqs=global_reqs, - allow_3_only=True, - ) - ) - class TestBackportPythonMarkers(testtools.TestCase): def setUp(self): @@ -581,7 +475,6 @@ def test_notmatching_no_backport(self): self.req, self.global_reqs["name"], list(backports.keys()), - allow_3_only=True, ) ) @@ -595,6 +488,5 @@ def test_notmatching_with_backport(self): self.req, self.global_reqs["name"], list(backports.keys()), - allow_3_only=True, ) ) diff --git a/openstack_requirements/tests/test_project.py b/openstack_requirements/tests/test_project.py index a90146d66..e2139c236 100644 --- a/openstack_requirements/tests/test_project.py +++ b/openstack_requirements/tests/test_project.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from io import StringIO import os import textwrap @@ -56,6 +57,73 @@ def test_empty(self): ) +class TestVerifyPyprojectToml(testtools.TestCase): + def setUp(self): + super().setUp() + self.stderr = StringIO() + self.useFixture(fixtures.MonkeyPatch('sys.stderr', self.stderr)) + + def test_valid(self): + root = self.useFixture(common.pep_518_fixture).root + self.assertTrue(project.verify_pyproject_toml(root)) + self.assertEqual('', self.stderr.getvalue()) + + def test_missing_file(self): + root = self.useFixture(fixtures.TempDir()).path + self.assertFalse(project.verify_pyproject_toml(root)) + self.assertEqual( + 'Missing pyproject.toml file\n', self.stderr.getvalue() + ) + + def test_missing_build_system(self): + root = self.useFixture(fixtures.TempDir()).path + with open(os.path.join(root, 'pyproject.toml'), 'w') as fh: + fh.write( + textwrap.dedent(""" + [project] + name = "foo" + """) + ) + self.assertFalse(project.verify_pyproject_toml(root)) + self.assertIn( + "pyproject.toml is missing 'build-system' table", + self.stderr.getvalue(), + ) + + def test_invalid_build_backend(self): + root = self.useFixture(fixtures.TempDir()).path + with open(os.path.join(root, 'pyproject.toml'), 'w') as fh: + fh.write( + textwrap.dedent(""" + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + """) + ) + self.assertFalse(project.verify_pyproject_toml(root)) + self.assertIn( + "pyproject.toml has invalid 'build-system.build-backend'. " + "Expected 'pbr.build'; got 'setuptools.build_meta'", + self.stderr.getvalue(), + ) + + def test_missing_project_table(self): + root = self.useFixture(fixtures.TempDir()).path + with open(os.path.join(root, 'pyproject.toml'), 'w') as fh: + fh.write( + textwrap.dedent(""" + [build-system] + requires = ["pbr>=6.0.0", "setuptools>=64.0.0"] + build-backend = "pbr.build" + """) + ) + self.assertTrue(project.verify_pyproject_toml(root)) + self.assertIn( + "pyproject.toml is missing 'project' table", + self.stderr.getvalue(), + ) + + class TestProjectExtras(testtools.TestCase): def test_pyproject_toml(self): root = self.useFixture(fixtures.TempDir()).path diff --git a/playbooks/files/project-requirements-change.py b/playbooks/files/project-requirements-change.py index 1a997e486..1baf73733 100755 --- a/playbooks/files/project-requirements-change.py +++ b/playbooks/files/project-requirements-change.py @@ -18,7 +18,6 @@ import argparse import contextlib import os -import re import shlex import shutil import subprocess @@ -30,9 +29,6 @@ from openstack_requirements import requirement # noqa -PYTHON_3_BRANCH = re.compile(r'^stable\/[u-z].*') - - def run_command(cmd): print(cmd) cmd_list = shlex.split(str(cmd)) @@ -130,6 +126,14 @@ def main(): backports = {} cwd = os.getcwd() + + # Verify that pyproject.toml is present and contains the required + # attributes. We only do this on master since we don't want to be + # strict on already released branches + pyproject_found = None + if branch in ('master', 'main'): + pyproject_found = project.verify_pyproject_toml(cwd) + # build a list of requirements in the proposed change, # and check them for style violations while doing so head_proj = project.read(cwd) @@ -144,21 +148,23 @@ def main(): # either. head_strict = not branch.startswith('stable/') head_reqs.process(strict=head_strict) - # Starting with Ussuri and later, we only need to be strict about - # Python 3 requirements. - python_3_branch = head_strict or PYTHON_3_BRANCH.match(branch) failed = check.validate( head_reqs, denylist, global_reqs, list(backports.keys()), - allow_3_only=python_3_branch, ) # report the results + error = False if failed or head_reqs.failed: print("*** Incompatible requirement found!") + error = True + if pyproject_found is False: + print("*** Invalid or missing pyproject.toml!") + error = True + if error: print("*** See https://docs.openstack.org/requirements/latest/") sys.exit(1) diff --git a/upper-constraints.txt b/upper-constraints.txt index 3ad73d84c..9b4cdcadd 100644 --- a/upper-constraints.txt +++ b/upper-constraints.txt @@ -58,18 +58,19 @@ python-mistralclient===6.2.0 oslo.context===6.4.0 rcssmin===1.2.2 pycadf===4.0.1 -grpcio===1.81.0 +grpcio===1.81.1 sniffio===1.3.1 fixtures===4.3.2 -neutron-lib===4.0.0 +neutron-lib===4.1.0 XStatic-FileSaver===1.3.2.1 jaraco.functools===4.5.0 oslo.metrics===0.16.0 storage-interfaces===1.0.5 pydantic===2.13.4 +Hypercorn===0.18.0 pystache===0.6.8 XStatic-Font-Awesome===6.2.1.2 -aiohttp===3.14.0 +aiohttp===3.14.1 waitress===3.0.2 os-refresh-config===14.0.1 pysnmp===7.1.27 @@ -80,11 +81,11 @@ sphinx-copybutton===0.5.2 beartype===0.22.9 ddt===1.7.2 pyserial===3.5 -moto===5.2.1 +moto===5.2.2 infi.dtypes.wwn===0.1.1 awscrt===0.34.1 pcre2===0.6.0 -python-freezerclient===6.3.0 +python-freezerclient===6.4.0 python-vitrageclient===5.4.0 py-pure-client===1.88.0 krest===1.3.8 @@ -129,11 +130,11 @@ jsonpointer===3.1.1 defusedxml===0.7.1 opentelemetry-sdk===1.42.1 netaddr===1.3.0 -pyghmi===1.6.16 +pyghmi===1.6.17 sphinxcontrib-blockdiag===3.0.0 aiosqlite===0.22.1 gnocchiclient===7.2.0 -wcwidth===0.8.0 +wcwidth===0.8.1 sphinxcontrib.datatemplates===0.11.0 jsonpath-rw===1.4.0 prettytable===3.17.0 @@ -147,7 +148,7 @@ async-timeout===5.0.1 virtualbmc===3.3.0 SQLAlchemy===2.0.50 pyroute2===0.8.1 -google-auth===2.53.0 +google-auth===2.54.0 kazoo===2.11.0 pyspnego===0.12.1 trio-websocket===0.12.2 @@ -177,7 +178,7 @@ python-subunit===1.4.6 pycparser===3.0 mock===5.2.0 PyYAML===6.0.3 -beautifulsoup4===4.14.3 +beautifulsoup4===4.15.0 ovs===3.7.1 cryptography===43.0.3 httpcore===1.0.9 @@ -197,7 +198,7 @@ tzlocal===5.3.1 sysv_ipc===1.2.0 sphinxcontrib-jsmath===1.0.1 django_compressor===4.6.0 -awscurl===0.42 +awscurl===0.44 trio===0.33.0 python-novaclient===18.13.0 pact===1.12.0 @@ -286,7 +287,7 @@ pydantic_core===2.46.4 uritemplate===4.2.0 docutils===0.21.2 threadpoolctl===3.6.0 -os-ken===4.2.0 +os-ken===4.2.1 ujson===5.12.1 selenium===4.44.0 pytest-subtests===0.15.0 @@ -300,7 +301,7 @@ capacity===1.3.14 playwright===1.60.0 markdown-it-py===4.2.0 retrying===1.4.2 -python-discovery===1.4.0 +python-discovery===1.4.2 platformdirs===4.10.0 pydotplus===2.0.2 boto3===1.35.99 @@ -354,7 +355,7 @@ keystonemiddleware===13.0.0 django-formtools===2.6.1 XStatic-Spin===1.2.5.3 rich===15.0.0 -os-traits===3.6.0 +os-traits===3.8.0 typepy===1.3.5 SecretStorage===3.5.0 XStatic-Rickshaw===1.5.1.3 @@ -385,9 +386,9 @@ python-consul===1.1.0 more-itertools===11.1.0 seqdiag===3.0.0 numpy===2.4.6 -msgpack===1.1.2 +msgpack===1.2.0 Sphinx===9.0.4 -oslo.config===10.4.0 +oslo.config===10.5.0 openstackdocstheme===3.6.0 osc-placement===4.8.0 rpds-py===2026.5.1 @@ -405,7 +406,7 @@ pytest-metadata===3.1.1 pyparsing===3.3.2 geomet===1.1.0 opentelemetry-exporter-otlp-proto-common===1.42.1 -distlib===0.4.1 +distlib===0.4.3 ast_serialize===0.5.0 dogpile.cache===1.5.0 python-barbicanclient===7.4.0 @@ -438,7 +439,7 @@ cotyledon===2.2.0 xattr===1.3.0 systemd-python===235 python-memcached===1.62 -openstacksdk===4.14.0 +openstacksdk===4.16.0 infi.dtypes.nqn===0.1.0 six===1.17.0 h2===4.3.0 @@ -453,14 +454,14 @@ requestsexceptions===1.4.0 testresources===2.1.2 falcon===4.2.0 tomlkit===0.15.0 -etcd3gw===2.6.0 +etcd3gw===2.7.0 Flask-RESTful===0.3.10 GitPython===3.1.50 python-ironicclient===6.1.0 babel===2.18.0 XStatic===1.0.3 XStatic-Angular-FileUpload===12.2.13.2 -python-openstackclient===10.0.0 +python-openstackclient===10.1.0 pyzmq===27.1.0 oslo.db===18.0.0 simplegeneric===0.8.1 @@ -556,16 +557,16 @@ importlib_metadata===9.0.0 sortedcontainers===2.4.0 microversion_parse===2.1.0 python-linstor===1.28.1 -filelock===3.29.1 +filelock===3.29.3 python-tackerclient===2.5.0 python-heatclient===5.2.0 -oslo.utils===10.1.0 +oslo.utils===10.1.1 requests-kerberos===0.15.0 itsdangerous===2.2.0 XStatic-jquery-ui===1.13.0.2 monasca-statsd===2.7.0 python-dateutil===2.9.0.post0 -virtualenv===21.4.2 +virtualenv===21.4.3 colorama===0.4.6 confetti===2.5.3 ironic-lib===7.0.0