7bca319451
This change is necessary in order to use the project's update-requirements.py script when preparing the environment on the Hyper-V compute nodes used in the Hyper-V CI. Using the mentioned script will set the latest requirements in nova, neutron, manila, etc., reducing the CI's amount of failures caused by outdated requirements. Including Microsoft Windows as a supported operating system on this project. Also, we update the edit-constraints.py and project.py files to support os.rename() of Windows. On Windows, os.rename() will create a file with the source name, without deleting the initial file first. Example of a traceback: Traceback (most recent call last): File "C:\Python27\Scripts\edit-constraints-script.py", line 9, in <module> load_entry_point('openstack.requirements==0.0.1.dev2497', 'console_scripts', 'edit-constraints')() File "C:\Python27\lib\site-packages\openstack_requirements\cmds\edit_constraint.py", line 74, in main os.rename(args[0] + '.tmp', args[0]) WindowsError: [Error 183] Cannot create a file when that file already exists This could be easily solved by removing the file before the rename. Proposed fix: Add before "os.rename(args[0] + '.tmp', args[0])" the line "os.remove(args[0])" in edit_constraint.py Add before "os.rename(tmpname, fullname)" the line "os.remove(fullname)" in project.py Add "Operating System :: Microsoft :: Windows" in setup.cfg DocImpact: Added Windows as a supported OS in setup.cfg Change-Id: If123a65fd8d49d5c67a1db16827a9617ce520dba Signed-off-by: Costin Galan <cgalan@cloudbasesolutions.com>
185 lines
6.4 KiB
Python
185 lines
6.4 KiB
Python
# Copyright 2012 OpenStack Foundation
|
|
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# 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.
|
|
|
|
"""The project abstraction."""
|
|
|
|
import collections
|
|
import errno
|
|
import io
|
|
import os
|
|
from six.moves import configparser
|
|
|
|
from parsley import makeGrammar
|
|
|
|
from openstack_requirements import requirement
|
|
|
|
# PURE logic from here until the IO marker below.
|
|
|
|
|
|
_Comment = collections.namedtuple('Comment', ['line'])
|
|
_Extra = collections.namedtuple('Extra', ['name', 'content'])
|
|
|
|
|
|
_extras_grammar = """
|
|
ini = (line*:p extras?:e line*:l final:s) -> (''.join(p), e, ''.join(l+[s]))
|
|
line = ~extras <(~'\\n' anything)* '\\n'>
|
|
final = <(~'\\n' anything)* >
|
|
extras = '[' 'e' 'x' 't' 'r' 'a' 's' ']' '\\n'+ body*:b -> b
|
|
body = comment | extra
|
|
comment = <'#' (~'\\n' anything)* '\\n'>:c '\\n'* -> comment(c)
|
|
extra = name:n ' '* '=' line:l cont*:c '\\n'* -> extra(n, ''.join([l] + c))
|
|
name = <(anything:x ?(x not in '\\n \\t='))+>
|
|
cont = ' '+ <(~'\\n' anything)* '\\n'>
|
|
"""
|
|
_extras_compiled = makeGrammar(
|
|
_extras_grammar, {"comment": _Comment, "extra": _Extra})
|
|
|
|
|
|
Error = collections.namedtuple('Error', ['message'])
|
|
File = collections.namedtuple('File', ['filename', 'content'])
|
|
StdOut = collections.namedtuple('StdOut', ['message'])
|
|
Verbose = collections.namedtuple('Verbose', ['message'])
|
|
|
|
|
|
def extras(project):
|
|
"""Return a dict of extra-name:content for the extras in setup.cfg."""
|
|
if 'setup.cfg' not in project:
|
|
return {}
|
|
c = configparser.SafeConfigParser()
|
|
c.readfp(io.StringIO(project['setup.cfg']))
|
|
if not c.has_section('extras'):
|
|
return {}
|
|
return dict(c.items('extras'))
|
|
|
|
|
|
def merge_setup_cfg(old_content, new_extras):
|
|
# This is ugly. All the existing libraries handle setup.cfg's poorly.
|
|
prefix, extras, suffix = _extras_compiled(old_content).ini()
|
|
out_extras = []
|
|
if extras is not None:
|
|
for extra in extras:
|
|
if type(extra) is _Comment:
|
|
out_extras.append(extra)
|
|
elif type(extra) is _Extra:
|
|
if extra.name not in new_extras:
|
|
out_extras.append(extra)
|
|
continue
|
|
e = _Extra(
|
|
extra.name,
|
|
requirement.to_content(
|
|
new_extras[extra.name], ':', ' ', False))
|
|
out_extras.append(e)
|
|
else:
|
|
raise TypeError('unknown type %r' % extra)
|
|
if out_extras:
|
|
extras_str = ['[extras]\n']
|
|
for extra in out_extras:
|
|
if type(extra) is _Comment:
|
|
extras_str.append(extra.line)
|
|
else:
|
|
extras_str.append(extra.name + ' =')
|
|
extras_str.append(extra.content)
|
|
if suffix:
|
|
extras_str.append('\n')
|
|
extras_str = ''.join(extras_str)
|
|
else:
|
|
extras_str = ''
|
|
return prefix + extras_str + suffix
|
|
|
|
|
|
# IO from here to the end of the file.
|
|
|
|
def _safe_read(project, filename, output=None):
|
|
if output is None:
|
|
output = project
|
|
try:
|
|
path = os.path.join(project['root'], filename)
|
|
with io.open(path, 'rt', encoding="utf-8") as f:
|
|
output[filename] = f.read()
|
|
except IOError as e:
|
|
if e.errno != errno.ENOENT:
|
|
raise
|
|
|
|
|
|
def read(root):
|
|
"""Read into memory the packaging data for the project at root.
|
|
|
|
:param root: A directory path.
|
|
:return: A dict representing the project with the following keys:
|
|
- root: The root dir.
|
|
- setup.py: Contents of setup.py.
|
|
- setup.cfg: Contents of setup.cfg.
|
|
- requirements: Dict of requirement file name: contents.
|
|
"""
|
|
result = {'root': root}
|
|
_safe_read(result, 'setup.py')
|
|
_safe_read(result, 'setup.cfg')
|
|
requirements = {}
|
|
result['requirements'] = requirements
|
|
target_files = [
|
|
'requirements.txt', 'tools/pip-requires',
|
|
'test-requirements.txt', 'tools/test-requires',
|
|
]
|
|
for py_version in (2, 3):
|
|
target_files.append('requirements-py%s.txt' % py_version)
|
|
target_files.append('test-requirements-py%s.txt' % py_version)
|
|
for target_file in target_files:
|
|
_safe_read(result, target_file, output=requirements)
|
|
return result
|
|
|
|
|
|
def write(project, actions, stdout, verbose, noop=False):
|
|
"""Write actions into project.
|
|
|
|
:param project: A project metadata dict.
|
|
:param actions: A list of action tuples - File or Verbose - that describe
|
|
what actions are to be taken.
|
|
Error objects write a message to stdout and trigger an exception at
|
|
the end of _write_project.
|
|
File objects describe a file to have content placed in it.
|
|
StdOut objects describe a message to write to stdout.
|
|
Verbose objects will write a message to stdout when verbose is True.
|
|
:param stdout: Where to write content for stdout.
|
|
:param verbose: If True Verbose actions will be written to stdout.
|
|
:param noop: If True nothing will be written to disk.
|
|
:return None:
|
|
:raises IOError: If the IO operations fail, IOError is raised. If this
|
|
happens some actions may have been applied and others not.
|
|
"""
|
|
error = False
|
|
for action in actions:
|
|
if type(action) is Error:
|
|
error = True
|
|
stdout.write(action.message + '\n')
|
|
elif type(action) is File:
|
|
if noop:
|
|
continue
|
|
fullname = os.path.join(project['root'], action.filename)
|
|
tmpname = fullname + '.tmp'
|
|
with open(tmpname, 'wt') as f:
|
|
f.write(action.content)
|
|
if os.path.exists(fullname):
|
|
os.remove(fullname)
|
|
os.rename(tmpname, fullname)
|
|
elif type(action) is StdOut:
|
|
stdout.write(action.message)
|
|
elif type(action) is Verbose:
|
|
if verbose:
|
|
stdout.write(u"%s\n" % (action.message,))
|
|
else:
|
|
raise Exception("Invalid action %r" % (action,))
|
|
if error:
|
|
raise Exception("Error occurred processing %s" % (project['root']))
|