@@ -25,18 +25,33 @@ INDEX = "https://pypi.io/pypi"
25
25
EXTENSIONS = ['tar.gz', 'tar.bz2', 'tar', 'zip', '.whl']
26
26
"""Permitted file extensions. These are evaluated from left to right and the first occurance is returned."""
27
27
28
- def _get_value(attribute, text):
29
- """Match attribute in text and return it."""
28
+ import logging
29
+ logging.basicConfig(level=logging.INFO)
30
+
31
+
32
+ def _get_values(attribute, text):
33
+ """Match attribute in text and return all matches.
34
+
35
+ :returns: List of matches.
36
+ """
30
37
regex = '{}\s+=\s+"(.*)";'.format(attribute)
31
38
regex = re.compile(regex)
32
- value = regex.findall(text)
33
- n = len(value)
39
+ values = regex.findall(text)
40
+ return values
41
+
42
+ def _get_unique_value(attribute, text):
43
+ """Match attribute in text and return unique match.
44
+
45
+ :returns: Single match.
46
+ """
47
+ values = _get_values(attribute, text)
48
+ n = len(values)
34
49
if n > 1:
35
- raise ValueError("Found too many values for {}".format(attribute))
50
+ raise ValueError("found too many values for {}".format(attribute))
36
51
elif n == 1:
37
- return value [0]
52
+ return values [0]
38
53
else:
39
- raise ValueError("No value found for {}".format(attribute))
54
+ raise ValueError("no value found for {}".format(attribute))
40
55
41
56
def _get_line_and_value(attribute, text):
42
57
"""Match attribute in text. Return the line and the value of the attribute."""
@@ -45,11 +60,11 @@ def _get_line_and_value(attribute, text):
45
60
value = regex.findall(text)
46
61
n = len(value)
47
62
if n > 1:
48
- raise ValueError("Found too many values for {}".format(attribute))
63
+ raise ValueError("found too many values for {}".format(attribute))
49
64
elif n == 1:
50
65
return value[0]
51
66
else:
52
- raise ValueError("No value found for {}".format(attribute))
67
+ raise ValueError("no value found for {}".format(attribute))
53
68
54
69
55
70
def _replace_value(attribute, value, text):
@@ -64,187 +79,163 @@ def _fetch_page(url):
64
79
if r.status_code == requests.codes.ok:
65
80
return r.json()
66
81
else:
67
- raise ValueError("Request for {} failed".format(url))
68
-
69
- def _get_latest_version(package, extension):
70
-
82
+ raise ValueError("request for {} failed".format(url))
71
83
84
+ def _get_latest_version_pypi(package, extension):
85
+ """Get latest version and hash from PyPI."""
72
86
url = "{}/{}/json".format(INDEX, package)
73
87
json = _fetch_page(url)
74
88
75
- data = extract_relevant_nix_data(json, extension)[1]
76
-
77
- version = data['latest_version']
78
- if version in data['versions']:
79
- sha256 = data['versions'][version]['sha256']
80
- else:
81
- sha256 = None # Its possible that no file was uploaded to PyPI
89
+ version = json['info']['version']
90
+ for release in json['releases'][version]:
91
+ if release['filename'].endswith(extension):
92
+ # TODO: In case of wheel we need to do further checks!
93
+ sha256 = release['digests']['sha256']
82
94
83
95
return version, sha256
84
96
85
97
86
- def extract_relevant_nix_data(json, extension):
87
- """Extract relevant Nix data from the JSON of a package obtained from PyPI.
98
+ def _get_latest_version_github(package, extension):
99
+ raise ValueError("updating from GitHub is not yet supported.")
100
+
101
+
102
+ FETCHERS = {
103
+ 'fetchFromGitHub' : _get_latest_version_github,
104
+ 'fetchPypi' : _get_latest_version_pypi,
105
+ 'fetchurl' : _get_latest_version_pypi,
106
+ }
88
107
89
- :param json: JSON obtained from PyPI
108
+
109
+ DEFAULT_SETUPTOOLS_EXTENSION = 'tar.gz'
110
+
111
+
112
+ FORMATS = {
113
+ 'setuptools' : DEFAULT_SETUPTOOLS_EXTENSION,
114
+ 'wheel' : 'whl'
115
+ }
116
+
117
+ def _determine_fetcher(text):
118
+ # Count occurences of fetchers.
119
+ nfetchers = sum(text.count('src = {}'.format(fetcher)) for fetcher in FETCHERS.keys())
120
+ if nfetchers == 0:
121
+ raise ValueError("no fetcher.")
122
+ elif nfetchers > 1:
123
+ raise ValueError("multiple fetchers.")
124
+ else:
125
+ # Then we check which fetcher to use.
126
+ for fetcher in FETCHERS.keys():
127
+ if 'src = {}'.format(fetcher) in text:
128
+ return fetcher
129
+
130
+
131
+ def _determine_extension(text, fetcher):
132
+ """Determine what extension is used in the expression.
133
+
134
+ If we use:
135
+ - fetchPypi, we check if format is specified.
136
+ - fetchurl, we determine the extension from the url.
137
+ - fetchFromGitHub we simply use `.tar.gz`.
90
138
"""
91
- def _extract_license(json):
92
- """Extract license from JSON."""
93
- return json['info']['license']
94
-
95
- def _available_versions(json):
96
- return json['releases'].keys()
97
-
98
- def _extract_latest_version(json):
99
- return json['info']['version']
100
-
101
- def _get_src_and_hash(json, version, extensions):
102
- """Obtain url and hash for a given version and list of allowable extensions."""
103
- if not json['releases']:
104
- msg = "Package {}: No releases available.".format(json['info']['name'])
105
- raise ValueError(msg)
106
- else:
107
- # We use ['releases'] and not ['urls'] because we want to have the possibility for different version.
108
- for possible_file in json['releases'][version]:
109
- for extension in extensions:
110
- if possible_file['filename'].endswith(extension):
111
- src = {'url': str(possible_file['url']),
112
- 'sha256': str(possible_file['digests']['sha256']),
113
- }
114
- return src
115
- else:
116
- msg = "Package {}: No release with valid file extension available.".format(json['info']['name'])
117
- logging.info(msg)
118
- return None
119
- #raise ValueError(msg)
120
-
121
- def _get_sources(json, extensions):
122
- versions = _available_versions(json)
123
- releases = {version: _get_src_and_hash(json, version, extensions) for version in versions}
124
- releases = toolz.itemfilter(lambda x: x[1] is not None, releases)
125
- return releases
126
-
127
- # Collect data)
128
- name = str(json['info']['name'])
129
- latest_version = str(_extract_latest_version(json))
130
- #src = _get_src_and_hash(json, latest_version, EXTENSIONS)
131
- sources = _get_sources(json, [extension])
132
-
133
- # Collect meta data
134
- license = str(_extract_license(json))
135
- license = license if license != "UNKNOWN" else None
136
- summary = str(json['info'].get('summary')).strip('.')
137
- summary = summary if summary != "UNKNOWN" else None
138
- #description = str(json['info'].get('description'))
139
- #description = description if description != "UNKNOWN" else None
140
- homepage = json['info'].get('home_page')
141
-
142
- data = {
143
- 'latest_version' : latest_version,
144
- 'versions' : sources,
145
- #'src' : src,
146
- 'meta' : {
147
- 'description' : summary if summary else None,
148
- #'longDescription' : description,
149
- 'license' : license,
150
- 'homepage' : homepage,
151
- },
152
- }
153
- return name, data
139
+ if fetcher == 'fetchPypi':
140
+ try:
141
+ format = _get_unique_value('format', text)
142
+ except ValueError as e:
143
+ format = None # format was not given
144
+
145
+ try:
146
+ extension = _get_unique_value('extension', text)
147
+ except ValueError as e:
148
+ extension = None # extension was not given
149
+
150
+ if extension is None:
151
+ if format is None:
152
+ format = 'setuptools'
153
+ extension = FORMATS[format]
154
+
155
+ elif fetcher == 'fetchurl':
156
+ url = _get_unique_value('url', text)
157
+ extension = os.path.splitext(url)[1]
158
+ if 'pypi' not in url:
159
+ raise ValueError('url does not point to PyPI.')
160
+
161
+ elif fetcher == 'fetchFromGitHub':
162
+ raise ValueError('updating from GitHub is not yet implemented.')
163
+
164
+ return extension
154
165
155
166
156
167
def _update_package(path):
157
168
169
+
170
+
171
+ # Read the expression
172
+ with open(path, 'r') as f:
173
+ text = f.read()
174
+
175
+ # Determine pname.
176
+ pname = _get_unique_value('pname', text)
177
+
178
+ # Determine version.
179
+ version = _get_unique_value('version', text)
180
+
181
+ # First we check how many fetchers are mentioned.
182
+ fetcher = _determine_fetcher(text)
183
+
184
+ extension = _determine_extension(text, fetcher)
185
+
186
+ new_version, new_sha256 = _get_latest_version_pypi(pname, extension)
187
+
188
+ if new_version == version:
189
+ logging.info("Path {}: no update available for {}.".format(path, pname))
190
+ return False
191
+ if not new_sha256:
192
+ raise ValueError("no file available for {}.".format(pname))
193
+
194
+ text = _replace_value('version', new_version, text)
195
+ text = _replace_value('sha256', new_sha256, text)
196
+
197
+ with open(path, 'w') as f:
198
+ f.write(text)
199
+
200
+ logging.info("Path {}: updated {} from {} to {}".format(path, pname, version, new_version))
201
+
202
+ return True
203
+
204
+
205
+ def _update(path):
206
+
158
207
# We need to read and modify a Nix expression.
159
208
if os.path.isdir(path):
160
209
path = os.path.join(path, 'default.nix')
161
210
211
+ # If a default.nix does not exist, we quit.
162
212
if not os.path.isfile(path):
163
- logging.warning ("Path does not exist: {} ".format(path))
213
+ logging.info ("Path {}: does not exist. ".format(path))
164
214
return False
165
215
216
+ # If file is not a Nix expression, we quit.
166
217
if not path.endswith(".nix"):
167
- logging.warning("Path does not end with `.nix`, skipping: {}".format(path))
168
- return False
169
-
170
- with open(path, 'r') as f:
171
- text = f.read()
172
-
173
- try:
174
- pname = _get_value('pname', text)
175
- except ValueError as e:
176
- logging.warning("Path {}: {}".format(path, str(e)))
218
+ logging.info("Path {}: does not end with `.nix`.".format(path))
177
219
return False
178
220
179
221
try:
180
- version = _get_value('version', text )
222
+ return _update_package(path )
181
223
except ValueError as e:
182
- logging.warning("Path {}: {}".format(path, str(e) ))
224
+ logging.warning("Path {}: {}".format(path, e ))
183
225
return False
184
226
185
- # If we use a wheel, then we need to request a wheel as well
186
- try:
187
- format = _get_value('format', text)
188
- except ValueError as e:
189
- # No format mentioned, then we assume we have setuptools
190
- # and use a .tar.gz
191
- logging.info("Path {}: {}".format(path, str(e)))
192
- extension = ".tar.gz"
193
- else:
194
- if format == 'wheel':
195
- extension = ".whl"
196
- else:
197
- try:
198
- url = _get_value('url', text)
199
- extension = os.path.splitext(url)[1]
200
- if 'pypi' not in url:
201
- logging.warning("Path {}: uses non-PyPI url, not updating.".format(path))
202
- return False
203
- except ValueError as e:
204
- logging.info("Path {}: {}".format(path, str(e)))
205
- extension = ".tar.gz"
206
-
207
- try:
208
- new_version, new_sha256 = _get_latest_version(pname, extension)
209
- except ValueError as e:
210
- logging.warning("Path {}: {}".format(path, str(e)))
211
- else:
212
- if not new_sha256:
213
- logging.warning("Path has no valid file available: {}".format(path))
214
- return False
215
- if new_version != version:
216
- try:
217
- text = _replace_value('version', new_version, text)
218
- except ValueError as e:
219
- logging.warning("Path {}: {}".format(path, str(e)))
220
- try:
221
- text = _replace_value('sha256', new_sha256, text)
222
- except ValueError as e:
223
- logging.warning("Path {}: {}".format(path, str(e)))
224
-
225
- with open(path, 'w') as f:
226
- f.write(text)
227
-
228
- logging.info("Updated {} from {} to {}".format(pname, version, new_version))
229
-
230
- else:
231
- logging.info("No update available for {} at {}".format(pname, version))
232
-
233
- return True
234
-
235
-
236
227
def main():
237
228
238
229
parser = argparse.ArgumentParser()
239
230
parser.add_argument('package', type=str, nargs='+')
240
231
241
232
args = parser.parse_args()
242
233
243
- packages = args.package
234
+ packages = map(os.path.abspath, args.package)
244
235
245
- count = list(map(_update_package , packages))
236
+ count = list(map(_update , packages))
246
237
247
- # logging.info("{} package(s) updated".format(sum(count)))
238
+ logging.info("{} package(s) updated".format(sum(count)))
248
239
249
240
if __name__ == '__main__':
250
241
main()
0 commit comments