requirements/openstack_requirements/project.py
Costin Galan 7bca319451 Add Windows as supported operating system
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>
2016-02-26 14:25:50 +02:00

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']))