909d616cf4
TC tags framework is discontinued [1], so the banner is not needed anymore in our list-changes command as it won't be triggered. [1] http://lists.openstack.org/pipermail/openstack-discuss/2022-February/027214.html Change-Id: Iff26be4cc1701bf31a1030c10fce0b2486da3e85
163 lines
5.2 KiB
Python
163 lines
5.2 KiB
Python
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""Tools for working with requirements lists."""
|
|
|
|
import logging
|
|
import os.path
|
|
|
|
import pkg_resources
|
|
|
|
from openstack_releases import gitutils
|
|
from openstack_releases import processutils
|
|
from openstack_releases import pythonutils
|
|
from openstack_releases import versionutils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def find_bad_lower_bound_increases(workdir, repo,
|
|
start_version, new_version, hash,
|
|
report):
|
|
if (new_version.split('.')[-1] == '0' or
|
|
versionutils.looks_like_preversion(new_version)):
|
|
# There is no need to look at the requirements so don't do the
|
|
# extra work.
|
|
return
|
|
start_reqs = get_requirements_at_ref(workdir, repo, start_version)
|
|
hash_reqs = get_requirements_at_ref(workdir, repo, hash)
|
|
compare_lower_bounds(start_reqs, hash_reqs, report)
|
|
|
|
|
|
def compare_lower_bounds(start_reqs, hash_reqs, report):
|
|
for (section, name), req in sorted(hash_reqs.items()):
|
|
if section:
|
|
display_name = '[{}]{}'.format(section, name)
|
|
else:
|
|
display_name = name
|
|
|
|
if (section, name) not in start_reqs:
|
|
report('New dependency {}'.format(display_name))
|
|
|
|
else:
|
|
old_req = start_reqs[(section, name)]
|
|
old_min = get_min_specifier(old_req.specifier)
|
|
new_min = get_min_specifier(req.specifier)
|
|
|
|
if old_min is None and new_min is None:
|
|
# No minimums are specified.
|
|
continue
|
|
|
|
if old_min is None:
|
|
# There is no minimum on the old dependency but there
|
|
# is now a new minimum.
|
|
report(('Added minimum version for dependency {} of {} '
|
|
'without at least incrementing minor number').format(
|
|
display_name, new_min))
|
|
continue
|
|
|
|
if new_min is None:
|
|
# There was a minimum but it has been removed.
|
|
continue
|
|
|
|
if old_min.version not in req.specifier:
|
|
# The old minimum is not in the new spec.
|
|
report(('Changed supported versions for dependency {} from {} '
|
|
'to {} without at least incrementing minor number').format(
|
|
display_name, old_req.specifier, req.specifier))
|
|
|
|
|
|
def get_min_specifier(specifier_set):
|
|
"Find the specifier in the set that controls the lower bound."
|
|
for s in specifier_set:
|
|
if '>' in s.operator:
|
|
return s
|
|
|
|
|
|
def get_requirements_at_ref(workdir, repo, ref):
|
|
"""Check out the repo at the ref and load the list of requirements."""
|
|
body = ''
|
|
|
|
try:
|
|
dest = gitutils.clone_repo(workdir, repo, ref=ref)
|
|
processutils.check_call(['python3', 'setup.py', 'sdist'], cwd=dest)
|
|
sdist_name = pythonutils.get_sdist_name(workdir, repo)
|
|
requirements_filename = os.path.join(
|
|
dest, sdist_name + '.egg-info', 'requires.txt',
|
|
)
|
|
if os.path.exists(requirements_filename):
|
|
with open(requirements_filename, 'r') as f:
|
|
body = f.read()
|
|
else:
|
|
# The package has no dependencies.
|
|
pass
|
|
except Exception:
|
|
# We've had a few cases where a previous version had an issue and could
|
|
# no longer be installed. In this case, just move along.
|
|
LOG.warning('Unable to create sdist, unable to get requirements.')
|
|
LOG.warning('!!! Perform manual comparison for requirements changes'
|
|
'!!!')
|
|
|
|
return parse_requirements(body)
|
|
|
|
|
|
def parse_requirements(body):
|
|
"""Given the requires.txt file for an sdist, parse it.
|
|
|
|
Returns a dictionary mapping (section, pkg name) to the
|
|
requirements specifier.
|
|
|
|
Parses files that look like:
|
|
|
|
pbr>=1.6
|
|
keyring==7.3
|
|
requests>=2.5.2
|
|
PyYAML>=3.1.0
|
|
yamlordereddictloader
|
|
prompt_toolkit
|
|
tqdm
|
|
packaging>=15.2
|
|
mwclient==0.8.1
|
|
jsonschema>=2.6.0
|
|
Jinja2>=2.6
|
|
parawrap
|
|
reno>=2.0.0
|
|
sphinx>=1.6.2
|
|
|
|
[sphinxext]
|
|
sphinx<1.6.1,>=1.5.1
|
|
openstackdocstheme
|
|
sphinxcontrib.datatemplates
|
|
icalendar
|
|
|
|
"""
|
|
requirements = {}
|
|
section = ''
|
|
for line in body.splitlines():
|
|
# Ignore blank lines and comments
|
|
if (not line.strip()) or line.startswith('#'):
|
|
continue
|
|
if line.startswith('['):
|
|
section = line.lstrip('[').rstrip(']')
|
|
continue
|
|
|
|
try:
|
|
parsed_req = pkg_resources.Requirement.parse(line)
|
|
except ValueError:
|
|
LOG.warning('failed to parse %r', line)
|
|
else:
|
|
requirements[(section, parsed_req.name)] = parsed_req
|
|
|
|
return requirements
|