-
Notifications
You must be signed in to change notification settings - Fork 0
/
requirements.py
228 lines (201 loc) · 8.21 KB
/
requirements.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import os
import re
import logging
import warnings
from pkg_resources import Requirement as Req
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
__version__ = '0.1.0'
logging.basicConfig(level=logging.WARNING)
VCS = ['git', 'hg', 'svn', 'bzr']
class Requirement(object):
"""
This class is inspired from
https://github.com/davidfischer/requirements-parser/blob/master/requirements/requirement.py#L30
License: BSD
"""
def __init__(self, line):
self.line = line
self.is_editable = False
self.is_local_file = False
self.is_specifier = False
self.vcs = None
self.name = None
self.uri = None
self.full_uri = None
self.path = None
self.revision = None
self.scheme = None
self.login = None
self.extras = []
self.specs = []
def __repr__(self):
return '<Requirement: "{0}">'.format(self.line)
@classmethod
def parse(cls, line, editable=False):
"""
Parses a Requirement from an "editable" requirement which is either
a local project path or a VCS project URI.
See: pip/req.py:from_editable()
:param line: an "editable" requirement
:returns: a Requirement instance for the given line
:raises: ValueError on an invalid requirement
"""
if editable:
req = cls('-e {0}'.format(line))
req.is_editable = True
else:
req = cls(line)
url = urlparse(line)
req.uri = None
if url.scheme:
req.scheme = url.scheme
req.uri = url.scheme + '://' + url.netloc + url.path
fragment = url.fragment.split(' ')[0].strip()
req.name = fragment.split('egg=')[-1] or None
req.path = url.path
if fragment:
req.uri += '#{}'.format(fragment)
if url.username or url.password:
username = url.username or ''
password = url.password or ''
req.login = username + ':' + password
if '@' in url.path:
req.revision = url.path.split('@')[-1]
for vcs in VCS:
if req.uri.startswith(vcs):
req.vcs = vcs
if req.scheme.startswith('file://'):
req.is_local_file = True
if not req.vcs and not req.is_local_file and 'egg=' not in line:
# This is a requirement specifier.
# Delegate to pkg_resources and hope for the best
req.is_specifier = True
pkg_req = Req.parse(line)
req.name = pkg_req.unsafe_name
req.extras = list(pkg_req.extras)
req.specs = pkg_req.specs
if req.specs:
req.specs = sorted(req.specs)
return req
class Requirements:
def __init__(
self,
requirements="requirements.txt",
tests_requirements="requirements/tests.txt"):
self.requirements_path = requirements
self.tests_requirements_path = tests_requirements
def format_specifiers(self, requirement):
return ', '.join(
['{} {}'.format(s[0], s[1]) for s in requirement.specs])
@property
def install_requires(self):
dependencies = []
for requirement in self.parse(self.requirements_path):
if not requirement.is_editable and not requirement.uri \
and not requirement.vcs:
full_name = requirement.name
specifiers = self.format_specifiers(requirement)
if specifiers:
full_name = "{} {}".format(full_name, specifiers)
dependencies.append(full_name)
for requirement in self.get_dependency_links():
print(":: (base:install_requires) {}".format(requirement.name))
dependencies.append(requirement.name)
return dependencies
@property
def tests_require(self):
dependencies = []
for requirement in self.parse(self.tests_requirements_path):
if not requirement.is_editable and not requirement.uri \
and not requirement.vcs:
full_name = requirement.name
specifiers = self.format_specifiers(requirement)
if specifiers:
full_name = "{} {}".format(full_name, specifiers)
print(":: (tests:tests_require) {}".format(full_name))
dependencies.append(full_name)
return dependencies
@property
def dependency_links(self):
dependencies = []
for requirement in self.parse(self.requirements_path):
if requirement.uri or requirement.vcs or requirement.path:
print(":: (base:dependency_links) {}".format(
requirement.uri))
dependencies.append(requirement.uri)
return dependencies
@property
def dependencies(self):
install_requires = self.install_requires
dependency_links = self.dependency_links
tests_require = self.tests_require
if dependency_links:
print(
"\n"
"!! Some dependencies are linked to repository or local path.")
print(
"!! You'll need to run pip with following option: "
"`--process-dependency-links`"
"\n")
return {
'install_requires': install_requires,
'dependency_links': dependency_links,
'tests_require': tests_require}
def get_dependency_links(self):
dependencies = []
for requirement in self.parse(self.requirements_path):
if requirement.uri or requirement.vcs or requirement.path:
dependencies.append(requirement)
return dependencies
def parse(self, path=None):
path = path or self.requirements_path
path = os.path.abspath(path)
base_directory = os.path.dirname(path)
if not os.path.exists(path):
warnings.warn(
'Requirements file: {} does not exists.'.format(path))
return
with open(path) as requirements:
for index, line in enumerate(requirements.readlines()):
index += 1
line = line.strip()
if not line:
logging.debug('Empty line (line {} from {})'.format(
index, path))
continue
elif line.startswith('#'):
logging.debug(
'Comments line (line {} from {})'.format(index, path))
elif line.startswith('-f') or \
line.startswith('--find-links') or \
line.startswith('-i') or \
line.startswith('--index-url') or \
line.startswith('--extra-index-url') or \
line.startswith('--no-index'):
warnings.warn('Private repos not supported. Skipping.')
continue
elif line.startswith('-Z') or line.startswith(
'--always-unzip'):
warnings.warn('Unused option --always-unzip. Skipping.')
continue
elif line.startswith('-r') or line.startswith('--requirement'):
logging.debug(
'Pining to another requirements file '
'(line {} from {})'.format(index, path))
for _line in self.parse(path=os.path.join(
base_directory, line.split()[1])):
yield _line
elif line.startswith('-e') or line.startswith('--editable'):
# Editable installs are either a local project path
# or a VCS project URI
yield Requirement.parse(
re.sub(r'^(-e|--editable=?)\s*', '', line),
editable=True)
else:
logging.debug('Found "{}" (line {} from {})'.format(
line, index, path))
yield Requirement.parse(line, editable=False)
r = Requirements()