5
5
import sys
6
6
from dataclasses import dataclass , field
7
7
8
+ import jsonschema
8
9
import rich_click as click
9
10
import yaml
10
11
from dateutil .relativedelta import relativedelta
18
19
19
20
click .rich_click .SHOW_ARGUMENTS = True
20
21
21
- channels = ["conda-forge" ]
22
- platforms = ["noarch" , "linux-64" ]
23
- ignored_packages = [
24
- "coveralls" ,
25
- "hypothesis" ,
26
- "pip" ,
27
- "pytest" ,
28
- "pytest-cov" ,
29
- "pytest-env" ,
30
- "pytest-mypy-plugins" ,
31
- "pytest-timeout" ,
32
- "pytest-xdist" ,
33
- ]
22
+
23
+ schema = {
24
+ "type" : "object" ,
25
+ "properties" : {
26
+ "channels" : {"type" : "array" , "items" : {"type" : "string" }},
27
+ "platforms" : {"type" : "array" , "items" : {"type" : "string" }},
28
+ "policy" : {
29
+ "type" : "object" ,
30
+ "properties" : {
31
+ "packages" : {
32
+ "type" : "object" ,
33
+ "patternProperties" : {
34
+ "^[a-z][-a-z_]*$" : {"type" : "integer" , "minimum" : 1 }
35
+ },
36
+ "additionalProperties" : False ,
37
+ },
38
+ "default" : {"type" : "integer" , "minimum" : 1 },
39
+ "overrides" : {
40
+ "type" : "object" ,
41
+ "patternProperties" : {
42
+ "^[a-z][-a-z_]*" : {"type" : "string" , "format" : "date" }
43
+ },
44
+ "additionalProperties" : False ,
45
+ },
46
+ "exclude" : {"type" : "array" , "items" : {"type" : "string" }},
47
+ "ignored_violations" : {
48
+ "type" : "array" ,
49
+ "items" : {"type" : "string" , "pattern" : "^[a-z][-a-z_]*$" },
50
+ },
51
+ },
52
+ "required" : [
53
+ "packages" ,
54
+ "default" ,
55
+ "overrides" ,
56
+ "exclude" ,
57
+ "ignored_violations" ,
58
+ ],
59
+ },
60
+ },
61
+ "required" : ["channels" , "platforms" , "policy" ],
62
+ }
34
63
35
64
36
65
@dataclass
37
66
class Policy :
38
67
package_months : dict
39
68
default_months : int
69
+
70
+ channels : list [str ] = field (default_factory = list )
71
+ platforms : list [str ] = field (default_factory = list )
72
+
40
73
overrides : dict [str , Version ] = field (default_factory = dict )
41
74
75
+ ignored_violations : list [str ] = field (default_factory = list )
76
+ exclude : list [str ] = field (default_factory = list )
77
+
42
78
def minimum_version (self , today , package_name , releases ):
43
79
if (override := self .overrides .get (package_name )) is not None :
44
80
return find_release (releases , version = override )
@@ -117,6 +153,28 @@ def parse_environment(text):
117
153
return specs , warnings
118
154
119
155
156
+ def parse_policy (file ):
157
+ policy = yaml .safe_load (file )
158
+ try :
159
+ jsonschema .validate (instance = policy , schema = schema )
160
+ except jsonschema .ValidationError as e :
161
+ raise jsonschema .ValidationError (
162
+ f"Invalid policy definition: { str (e )} "
163
+ ) from None
164
+
165
+ package_policy = policy ["policy" ]
166
+
167
+ return Policy (
168
+ channels = policy ["channels" ],
169
+ platforms = policy ["platforms" ],
170
+ exclude = package_policy ["exclude" ],
171
+ package_months = package_policy ["packages" ],
172
+ default_months = package_policy ["default" ],
173
+ ignored_violations = package_policy ["ignored_violations" ],
174
+ overrides = package_policy ["overrides" ],
175
+ )
176
+
177
+
120
178
def is_preview (version ):
121
179
candidates = {"rc" , "b" , "a" }
122
180
@@ -175,11 +233,15 @@ def lookup_spec_release(spec, releases):
175
233
return releases [spec .name ][version ]
176
234
177
235
178
- def compare_versions (environments , policy_versions ):
236
+ def compare_versions (environments , policy_versions , ignored_violations ):
179
237
status = {}
180
238
for env , specs in environments .items ():
181
239
env_status = any (
182
- spec .version > policy_versions [spec .name ].version for spec in specs
240
+ (
241
+ spec .name not in ignored_violations
242
+ and spec .version > policy_versions [spec .name ].version
243
+ )
244
+ for spec in specs
183
245
)
184
246
status [env ] = env_status
185
247
return status
@@ -194,7 +256,7 @@ def version_comparison_symbol(required, policy):
194
256
return "="
195
257
196
258
197
- def format_bump_table (specs , policy_versions , releases , warnings ):
259
+ def format_bump_table (specs , policy_versions , releases , warnings , ignored_violations ):
198
260
table = Table (
199
261
Column ("Package" , width = 20 ),
200
262
Column ("Required" , width = 8 ),
@@ -221,7 +283,10 @@ def format_bump_table(specs, policy_versions, releases, warnings):
221
283
required_date = lookup_spec_release (spec , releases ).timestamp
222
284
223
285
status = version_comparison_symbol (required_version , policy_version )
224
- style = styles [status ]
286
+ if status == ">" and spec .name in ignored_violations :
287
+ style = warning_style
288
+ else :
289
+ style = styles [status ]
225
290
226
291
table .add_row (
227
292
spec .name ,
@@ -255,15 +320,26 @@ def format_bump_table(specs, policy_versions, releases, warnings):
255
320
return grid
256
321
257
322
323
+ def parse_date (string ):
324
+ if not string :
325
+ return None
326
+
327
+ return datetime .datetime .strptime (string , "%Y-%m-%d" ).date ()
328
+
329
+
258
330
@click .command ()
259
331
@click .argument (
260
332
"environment_paths" ,
261
333
type = click .Path (exists = True , readable = True , path_type = pathlib .Path ),
262
334
nargs = - 1 ,
263
335
)
264
- def main (environment_paths ):
336
+ @click .option ("--today" , type = parse_date , default = None )
337
+ @click .option ("--policy" , "policy_file" , type = click .File (mode = "r" ), required = True )
338
+ def main (today , policy_file , environment_paths ):
265
339
console = Console ()
266
340
341
+ policy = parse_policy (policy_file )
342
+
267
343
parsed_environments = {
268
344
path .stem : parse_environment (path .read_text ()) for path in environment_paths
269
345
}
@@ -272,30 +348,22 @@ def main(environment_paths):
272
348
env : dict (warnings_ ) for env , (_ , warnings_ ) in parsed_environments .items ()
273
349
}
274
350
environments = {
275
- env : [spec for spec in specs if spec .name not in ignored_packages ]
351
+ env : [spec for spec in specs if spec .name not in policy . exclude ]
276
352
for env , (specs , _ ) in parsed_environments .items ()
277
353
}
278
354
279
355
all_packages = list (
280
356
dict .fromkeys (spec .name for spec in concat (environments .values ()))
281
357
)
282
358
283
- policy_months = {
284
- "python" : 30 ,
285
- "numpy" : 18 ,
286
- }
287
- policy_months_default = 12
288
- overrides = {}
289
-
290
- policy = Policy (
291
- policy_months , default_months = policy_months_default , overrides = overrides
292
- )
293
-
294
359
gateway = Gateway ()
295
- query = gateway .query (channels , platforms , all_packages , recursive = False )
360
+ query = gateway .query (
361
+ policy .channels , policy .platforms , all_packages , recursive = False
362
+ )
296
363
records = asyncio .run (query )
297
364
298
- today = datetime .date .today ()
365
+ if today is None :
366
+ today = datetime .date .today ()
299
367
package_releases = pipe (
300
368
records ,
301
369
concat ,
@@ -307,13 +375,19 @@ def main(environment_paths):
307
375
package_releases ,
308
376
curry (find_policy_versions , policy , today ),
309
377
)
310
- status = compare_versions (environments , policy_versions )
378
+ status = compare_versions (environments , policy_versions , policy . ignored_violations )
311
379
312
380
release_lookup = {
313
381
n : {r .version : r for r in releases } for n , releases in package_releases .items ()
314
382
}
315
383
grids = {
316
- env : format_bump_table (specs , policy_versions , release_lookup , warnings [env ])
384
+ env : format_bump_table (
385
+ specs ,
386
+ policy_versions ,
387
+ release_lookup ,
388
+ warnings [env ],
389
+ policy .ignored_violations ,
390
+ )
317
391
for env , specs in environments .items ()
318
392
}
319
393
root_grid = Table .grid ()
0 commit comments