Completed
Pull Request — master (#2334)
by Edward
06:02
created

write_requirements()   F

Complexity

Conditions 10

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 44
rs 3.1304
cc 10

How to fix   Complexity   

Complexity

Complex classes like write_requirements() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python
2
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
3
# contributor license agreements.  See the NOTICE file distributed with
4
# this work for additional information regarding copyright ownership.
5
# The ASF licenses this file to You under the Apache License, Version 2.0
6
# (the "License"); you may not use this file except in compliance with
7
# the License.  You may obtain a copy of the License at
8
#
9
#     http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
17
"""
18
This script is used to automate generation of requirements.txt for st2 components.
19
20
The idea behind this script is that that each component has it's own requirements
21
"in-requirements.txt" file. in-requirements.txt is an input requirements file -
22
a requirements file with dependencies but WITHOUT any version restrictions.
23
24
In addition to this file, there's also the top-level "fixed-requirements.txt"
25
which pins production versions for the whole st2 stack. During production use
26
(building, packaging, etc) requirements.txt is generated from in-requirements.txt
27
where version of packages are fixed according to fixed-requirements.txt.
28
"""
29
30
import argparse
31
import os
32
import os.path
33
import sys
34
from distutils.version import StrictVersion
35
36
OSCWD = os.path.abspath(os.curdir)
37
GET_PIP = '    curl https://bootstrap.pypa.io/get-pip.py | python'
38
39
try:
40
    import pip
41
    from pip.req import parse_requirements
42
except ImportError:
43
    print 'Download pip:\n', GET_PIP
44
    sys.exit(1)
45
46
47
def parse_args():
48
    parser = argparse.ArgumentParser(description='Tool for requirements.txt generation.')
49
    parser.add_argument('-s', '--source-requirements', nargs='+',
50
                        required=True,
51
                        help='Specify paths to requirements file(s). '
52
                        'In case several requirements files are given their content is merged.')
53
    parser.add_argument('-f', '--fixed-requirements', required=True,
54
                        help='Specify path to fixed-requirements.txt file.')
55
    parser.add_argument('-o', '--output-file', default='requirements.txt',
56
                        help='Specify path to the resulting requirements file.')
57
    if len(sys.argv) < 2:
58
        parser.print_help()
59
        sys.exit(1)
60
    return vars(parser.parse_args())
61
62
63
def check_pip_version():
64
    if StrictVersion(pip.__version__) < StrictVersion('6.1.0'):
65
        print "Upgrade pip, your version `{0}' "\
66
              "is outdated:\n".format(pip.__version__), GET_PIP
67
        sys.exit(1)
68
69
70
def load_requirements(file_path):
71
    return tuple((r for r in parse_requirements(file_path, session=False)))
72
73
74
def locate_file(path, must_exist=False):
75
    if not os.path.isabs(path):
76
        path = os.path.join(OSCWD, path)
77
    if must_exist and not os.path.isfile(path):
78
        print("Error: couldn't locate file `{0}'".format(path))
79
    return path
80
81
82
def merge_source_requirements(sources):
83
    """
84
    Read requirements source files and merge it's content.
85
    """
86
    projects = set()
87
    merged_requirements = []
88
    for infile_path in (locate_file(p, must_exist=True) for p in sources):
89
        for req in load_requirements(infile_path):
90
            # Requirements starting with project name "project ..."
91
            if req.req:
92
                # Skip already added project name
93
                if req.req.project_name in projects:
94
                    continue
95
                projects.add(req.req.project_name)
96
                merged_requirements.append(req)
97
98
            # Requirements lines like "vcs+proto://url"
99
            elif req.link:
100
                merged_requirements.append(req)
101
            else:
102
                raise RuntimeError('Unexpected requirement {0}'.format(req))
103
104
    return merged_requirements
105
106
107
def write_requirements(sources=None, fixed_requirements=None, output_file=None):
108
    """
109
    Write resulting requirements taking versions from the fixed_requirements.
110
    """
111
    requirements = merge_source_requirements(sources)
112
    fixed = load_requirements(locate_file(fixed_requirements, must_exist=True))
113
114
    # Make sure there are no duplicate / conflicting definitions
115
    fixedreq_hash = {}
116
    for req in fixed:
117
        project_name = req.req.project_name
118
119
        if not req.req:
120
            continue
121
122
        if project_name in fixedreq_hash:
123
            raise ValueError('Duplicate definition for dependency "%s"' % (project_name))
124
125
        fixedreq_hash[project_name] = req
126
127
    lines_to_write = []
128
    links = set()
129
    for req in requirements:
130
        # we don't have any idea how to process links, so just add them
131
        if req.link and req.link not in links:
132
            links.add(req.link)
133
            rline = str(req.link)
134
        elif req.req:
135
            project = req.req.project_name
136
            if project in fixedreq_hash:
137
                rline = str(fixedreq_hash[project].req)
138
            else:
139
                rline = str(req.req)
140
141
        lines_to_write.append(rline)
142
143
    # Sort the lines to guarantee a stable order
144
    lines_to_write = sorted(lines_to_write)
145
    data = '\n'.join(lines_to_write) + '\n'
146
    with open(output_file, 'w') as fp:
147
        fp.write('# Don\'t edit this file. It\'s generated automatically!\n')
148
        fp.write(data)
149
150
    print('Requirements written to: {0}'.format(output_file))
151
152
153
if __name__ == '__main__':
154
    check_pip_version()
155
    args = parse_args()
156
    write_requirements(sources=args['source_requirements'],
157
                       fixed_requirements=args['fixed_requirements'],
158
                       output_file=args['output_file'])
159