releases/openstack_releases/requirements.py
Doug Hellmann bfd96f4fc0 wrap subprocess to capture output in tests
Provide some wrappers around subprocess functions to capture stderr
and stdout and divert it to the logs so that the test output is less
cluttered with the output of various git commands.

Change-Id: If36ef013aca498e3a0a9cc3a2b78d666775439ab
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
2018-02-08 05:14:13 -06:00

155 lines
4.8 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."
dest = gitutils.clone_repo(workdir, repo, ref=ref)
processutils.check_call(['python', '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.
body = ''
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
pyfiglet>=0.7.5
[sphinxext]
sphinx<1.6.1,>=1.5.1
oslosphinx
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