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
+ "exclude" : {"type" : "array" , "items" : {"type" : "string" }},
27
+ "channels" : {"type" : "array" , "items" : {"type" : "string" }},
28
+ "platforms" : {"type" : "array" , "items" : {"type" : "string" }},
29
+ "overrides" : {
30
+ "type" : "object" ,
31
+ "patternProperties" : {
32
+ "^[a-z][-a-z_]*" : {"type" : "string" , "format" : "date" }
33
+ },
34
+ "additionalProperties" : False ,
35
+ },
36
+ "policy" : {
37
+ "type" : "object" ,
38
+ "properties" : {
39
+ "packages" : {
40
+ "type" : "object" ,
41
+ "patternProperties" : {
42
+ "^[a-z][-a-z_]*$" : {"type" : "integer" , "minimum" : 1 }
43
+ },
44
+ "additionalProperties" : False ,
45
+ },
46
+ "default" : {"type" : "integer" , "minimum" : 1 },
47
+ "ignored_violations" : {
48
+ "type" : "array" ,
49
+ "items" : {"type" : "string" , "pattern" : "^[a-z][-a-z_]*$" },
50
+ },
51
+ },
52
+ "required" : ["packages" , "default" , "ignored_violations" ],
53
+ },
54
+ },
55
+ "required" : ["exclude" , "channels" , "platforms" , "overrides" ],
56
+ }
34
57
35
58
36
59
@dataclass
37
60
class Policy :
38
61
package_months : dict
39
62
default_months : int
63
+
64
+ channels : list [str ] = field (default_factory = list )
65
+ platforms : list [str ] = field (default_factory = list )
66
+
40
67
overrides : dict [str , Version ] = field (default_factory = dict )
41
68
69
+ ignored_violations : list [str ] = field (default_factory = list )
70
+ exclude : list [str ] = field (default_factory = list )
71
+
42
72
def minimum_version (self , today , package_name , releases ):
43
73
if (override := self .overrides .get (package_name )) is not None :
44
74
return find_release (releases , version = override )
@@ -117,6 +147,28 @@ def parse_environment(text):
117
147
return specs , warnings
118
148
119
149
150
+ def parse_policy (file ):
151
+ policy = yaml .safe_load (file )
152
+ try :
153
+ jsonschema .validate (instance = policy , schema = schema )
154
+ except jsonschema .ValidationError as e :
155
+ raise jsonschema .ValidationError (
156
+ f"Invalid policy definition: { str (e )} "
157
+ ) from None
158
+
159
+ package_policy = policy ["policy" ]
160
+
161
+ return Policy (
162
+ channels = policy ["channels" ],
163
+ platforms = policy ["platforms" ],
164
+ exclude = policy ["exclude" ],
165
+ package_months = package_policy ["packages" ],
166
+ default_months = package_policy ["default" ],
167
+ ignored_violations = package_policy ["ignored_violations" ],
168
+ overrides = policy ["overrides" ],
169
+ )
170
+
171
+
120
172
def is_preview (version ):
121
173
candidates = {"rc" , "b" , "a" }
122
174
@@ -175,11 +227,15 @@ def lookup_spec_release(spec, releases):
175
227
return releases [spec .name ][version ]
176
228
177
229
178
- def compare_versions (environments , policy_versions ):
230
+ def compare_versions (environments , policy_versions , ignored_violations ):
179
231
status = {}
180
232
for env , specs in environments .items ():
181
233
env_status = any (
182
- spec .version > policy_versions [spec .name ].version for spec in specs
234
+ (
235
+ spec .name not in ignored_violations
236
+ and spec .version > policy_versions [spec .name ].version
237
+ )
238
+ for spec in specs
183
239
)
184
240
status [env ] = env_status
185
241
return status
@@ -194,7 +250,7 @@ def version_comparison_symbol(required, policy):
194
250
return "="
195
251
196
252
197
- def format_bump_table (specs , policy_versions , releases , warnings ):
253
+ def format_bump_table (specs , policy_versions , releases , warnings , ignored_violations ):
198
254
table = Table (
199
255
Column ("Package" , width = 20 ),
200
256
Column ("Required" , width = 8 ),
@@ -221,7 +277,10 @@ def format_bump_table(specs, policy_versions, releases, warnings):
221
277
required_date = lookup_spec_release (spec , releases ).timestamp
222
278
223
279
status = version_comparison_symbol (required_version , policy_version )
224
- style = styles [status ]
280
+ if status == ">" and spec .name in ignored_violations :
281
+ style = warning_style
282
+ else :
283
+ style = styles [status ]
225
284
226
285
table .add_row (
227
286
spec .name ,
@@ -261,9 +320,12 @@ def format_bump_table(specs, policy_versions, releases, warnings):
261
320
type = click .Path (exists = True , readable = True , path_type = pathlib .Path ),
262
321
nargs = - 1 ,
263
322
)
264
- def main (environment_paths ):
323
+ @click .option ("--policy" , "policy_file" , type = click .File (mode = "r" ))
324
+ def main (policy_file , environment_paths ):
265
325
console = Console ()
266
326
327
+ policy = parse_policy (policy_file )
328
+
267
329
parsed_environments = {
268
330
path .stem : parse_environment (path .read_text ()) for path in environment_paths
269
331
}
@@ -272,27 +334,18 @@ def main(environment_paths):
272
334
env : dict (warnings_ ) for env , (_ , warnings_ ) in parsed_environments .items ()
273
335
}
274
336
environments = {
275
- env : [spec for spec in specs if spec .name not in ignored_packages ]
337
+ env : [spec for spec in specs if spec .name not in policy . exclude ]
276
338
for env , (specs , _ ) in parsed_environments .items ()
277
339
}
278
340
279
341
all_packages = list (
280
342
dict .fromkeys (spec .name for spec in concat (environments .values ()))
281
343
)
282
344
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
345
gateway = Gateway ()
295
- query = gateway .query (channels , platforms , all_packages , recursive = False )
346
+ query = gateway .query (
347
+ policy .channels , policy .platforms , all_packages , recursive = False
348
+ )
296
349
records = asyncio .run (query )
297
350
298
351
today = datetime .date .today ()
@@ -307,13 +360,19 @@ def main(environment_paths):
307
360
package_releases ,
308
361
curry (find_policy_versions , policy , today ),
309
362
)
310
- status = compare_versions (environments , policy_versions )
363
+ status = compare_versions (environments , policy_versions , policy . ignored_violations )
311
364
312
365
release_lookup = {
313
366
n : {r .version : r for r in releases } for n , releases in package_releases .items ()
314
367
}
315
368
grids = {
316
- env : format_bump_table (specs , policy_versions , release_lookup , warnings [env ])
369
+ env : format_bump_table (
370
+ specs ,
371
+ policy_versions ,
372
+ release_lookup ,
373
+ warnings [env ],
374
+ policy .ignored_violations ,
375
+ )
317
376
for env , specs in environments .items ()
318
377
}
319
378
root_grid = Table .grid ()
0 commit comments