40
40
# (at your option) any later version.
41
41
# https://www.gnu.org/licenses/
42
42
# ****************************************************************************
43
+ from typing import Any , Dict , List , NamedTuple , Optional , Tuple , Union
43
44
44
45
import sage .env
45
46
@@ -157,15 +158,13 @@ def pip_installed_packages(normalization=None):
157
158
stderr = devnull ,
158
159
)
159
160
stdout = proc .communicate ()[0 ].decode ()
160
-
161
- def normalize (name ):
161
+ def normalize (name : str ):
162
162
if normalization is None :
163
163
return name
164
164
elif normalization == 'spkg' :
165
165
return name .lower ().replace ('-' , '_' ).replace ('.' , '_' )
166
166
else :
167
167
raise NotImplementedError (f'normalization { normalization } is not implemented' )
168
-
169
168
try :
170
169
return {normalize (package ['name' ]): package ['version' ]
171
170
for package in json .loads (stdout )}
@@ -174,12 +173,50 @@ def normalize(name):
174
173
# This may happen if pip is not correctly installed.
175
174
return {}
176
175
177
- def list_packages (* pkg_types , ** opts ):
176
+ class PackageInfo (NamedTuple ):
177
+ """Represents information about a package."""
178
+ name : str
179
+ type : Optional [str ] = None
180
+ source : Optional [str ] = None
181
+ installed_version : Optional [str ] = None
182
+ remote_version : Optional [str ] = None
183
+
184
+ def is_installed (self ) -> bool :
185
+ return self .installed_version is not None
186
+
187
+ def __getitem__ (self , key : Union [int , str ]):
188
+ """
189
+ Only for backwards compatibility to allow dict-like access.
190
+
191
+ TESTS::
192
+
193
+ sage: package = PackageInfo("test_package")
194
+ sage: package["name"]
195
+ doctest:...: DeprecationWarning: dict-like access is deprecated, use e.g `pkg.name` instead of `pkg[name]`
196
+ See https://trac.sagemath.org/31013 for details.
197
+ test_package
198
+ sage: package[0]
199
+ test_package
200
+ """
201
+ if isinstance (key , str ):
202
+ from sage .misc .superseded import deprecation
203
+
204
+ if key == "installed" :
205
+ deprecation (31013 , "dict-like access via `installed` is deprecated, use `is_installed` instead" )
206
+ return self .is_installed ()
207
+ else :
208
+ deprecation (31013 , "dict-like access is deprecated, use e.g `pkg.name` instead of `pkg[name]`" )
209
+ return self .__getattribute__ (key )
210
+ else :
211
+ return tuple .__getitem__ (self , key )
212
+
213
+
214
+ def list_packages (* pkg_types : str , pkg_sources : List [str ] = ['normal' , 'pip' , 'script' ],
215
+ local : bool = False , ignore_URLError : bool = False , exclude_pip : bool = False ) -> Dict [str , PackageInfo ]:
178
216
r"""
179
217
Return a dictionary of information about each package.
180
218
181
- The keys are package names and values are dictionaries with the following
182
- keys:
219
+ The keys are package names and values are named tuples with the following keys:
183
220
184
221
- ``'type'``: either ``'base``, ``'standard'``, ``'optional'``, or ``'experimental'``
185
222
- ``'source'``: either ``'normal', ``'pip'``, or ``'script'``
@@ -219,46 +256,38 @@ def list_packages(*pkg_types, **opts):
219
256
...
220
257
'zn_poly']
221
258
sage: sage_conf_info = L['sage_conf'] # optional - build
222
- sage: sage_conf_info[' type'] # optional - build
259
+ sage: sage_conf_info. type # optional - build
223
260
'standard'
224
- sage: sage_conf_info[' installed'] # optional - build
261
+ sage: sage_conf_info. installed # optional - build
225
262
True
226
- sage: sage_conf_info[' source'] # optional - build
263
+ sage: sage_conf_info. source # optional - build
227
264
'script'
228
265
229
266
sage: L = list_packages(pkg_sources=['pip'], local=True) # optional - build internet
230
267
sage: bs4_info = L['beautifulsoup4'] # optional - build internet
231
- sage: bs4_info[' type'] # optional - build internet
268
+ sage: bs4_info. type # optional - build internet
232
269
'optional'
233
- sage: bs4_info[' source'] # optional - build internet
270
+ sage: bs4_info. source # optional - build internet
234
271
'pip'
235
272
236
273
Check the option ``exclude_pip``::
237
274
238
275
sage: [p for p, d in list_packages('optional', exclude_pip=True).items() # optional - build
239
- ....: if d[' source'] == 'pip']
276
+ ....: if d. source == 'pip']
240
277
[]
241
278
"""
242
279
if not pkg_types :
243
280
pkg_types = ('base' , 'standard' , 'optional' , 'experimental' )
244
281
elif any (pkg_type not in ('base' , 'standard' , 'optional' , 'experimental' ) for pkg_type in pkg_types ):
245
282
raise ValueError ("Each pkg_type must be one of 'base', 'standard', 'optional', 'experimental'" )
246
283
247
- pkg_sources = opts .pop ('pkg_sources' ,
248
- ('normal' , 'pip' , 'script' ))
249
-
250
- local = opts .pop ('local' , False )
251
- ignore_URLError = opts .pop ('ignore_URLError' , False )
252
- exclude_pip = opts .pop ('exclude_pip' , False )
253
284
if exclude_pip :
254
285
pkg_sources = [s for s in pkg_sources if s != 'pip' ]
255
- if opts :
256
- raise ValueError ("{} are not valid options" .format (sorted (opts )))
257
286
258
- pkgs = {p : {'name' : p , 'installed_version' : v , 'installed' : True ,
259
- 'remote_version' : None , 'source' : None }
287
+ pkgs = {p : PackageInfo (name = p , installed_version = v )
260
288
for p , v in installed_packages ('pip' not in pkg_sources ).items ()}
261
289
290
+ # Add additional information based on Sage's package repository
262
291
lp = []
263
292
SAGE_PKGS = sage .env .SAGE_PKGS
264
293
if not SAGE_PKGS :
@@ -287,33 +316,29 @@ def list_packages(*pkg_types, **opts):
287
316
else :
288
317
src = 'script'
289
318
290
- pkg = pkgs .get (p , dict ())
291
- pkgs [p ] = pkg
292
-
293
319
if typ not in pkg_types or src not in pkg_sources :
294
- del pkgs [p ]
320
+ try :
321
+ del pkgs [p ]
322
+ except KeyError :
323
+ pass
295
324
continue
296
325
297
- pkg .update ({'name' : p , 'type' : typ , 'source' : src })
298
- if pkg .get ('installed_version' , None ):
299
- pkg ['installed' ] = True
300
- else :
301
- pkg ['installed' ] = False
302
- pkg ['installed_version' ] = None
303
-
304
- if pkg ['source' ] == 'pip' :
326
+ if src == 'pip' :
305
327
if not local :
306
- pkg [ ' remote_version' ] = pip_remote_version (p , ignore_URLError = ignore_URLError )
328
+ remote_version = pip_remote_version (p , ignore_URLError = ignore_URLError )
307
329
else :
308
- pkg [ ' remote_version' ] = None
309
- elif pkg [ 'source' ] == 'normal' :
330
+ remote_version = None
331
+ elif src == 'normal' :
310
332
# If package-version.txt does not exist, that is an error
311
333
# in the build system => we just propagate the exception
312
334
package_filename = os .path .join (SAGE_PKGS , p , "package-version.txt" )
313
335
with open (package_filename ) as f :
314
- pkg [ ' remote_version' ] = f .read ().strip ()
336
+ remote_version = f .read ().strip ()
315
337
else :
316
- pkg ['remote_version' ] = 'none'
338
+ remote_version = None
339
+
340
+ pkg = pkgs .get (p , PackageInfo (name = p ))
341
+ pkgs [p ] = PackageInfo (p , typ , src , pkg .installed_version , remote_version )
317
342
318
343
return pkgs
319
344
@@ -422,7 +447,7 @@ def package_versions(package_type, local=False):
422
447
sage: std['zn_poly'] # optional - build, random
423
448
('0.9.p12', '0.9.p12')
424
449
"""
425
- return {pkg [ ' name' ] : (pkg [ ' installed_version' ] , pkg [ ' remote_version' ] ) for pkg in list_packages (package_type , local = local ).values ()}
450
+ return {pkg . name : (pkg . installed_version , pkg . remote_version ) for pkg in list_packages (package_type , local = local ).values ()}
426
451
427
452
428
453
def standard_packages ():
@@ -455,8 +480,8 @@ def standard_packages():
455
480
'the functions standard_packages, optional_packages, experimental_packages'
456
481
'are deprecated, use sage.features instead' )
457
482
pkgs = list_packages ('standard' , local = True ).values ()
458
- return (sorted (pkg [ ' name' ] for pkg in pkgs if pkg [ 'installed' ] ),
459
- sorted (pkg [ ' name' ] for pkg in pkgs if not pkg [ 'installed' ] ))
483
+ return (sorted (pkg . name for pkg in pkgs if pkg . is_installed () ),
484
+ sorted (pkg . name for pkg in pkgs if not pkg . is_installed () ))
460
485
461
486
462
487
def optional_packages ():
@@ -493,8 +518,8 @@ def optional_packages():
493
518
'are deprecated, use sage.features instead' )
494
519
pkgs = list_packages ('optional' , local = True )
495
520
pkgs = pkgs .values ()
496
- return (sorted (pkg [ ' name' ] for pkg in pkgs if pkg [ 'installed' ] ),
497
- sorted (pkg [ ' name' ] for pkg in pkgs if not pkg [ 'installed' ] ))
521
+ return (sorted (pkg . name for pkg in pkgs if pkg . is_installed () ),
522
+ sorted (pkg . name for pkg in pkgs if not pkg . is_installed () ))
498
523
499
524
500
525
def experimental_packages ():
@@ -525,8 +550,8 @@ def experimental_packages():
525
550
'the functions standard_packages, optional_packages, experimental_packages'
526
551
'are deprecated, use sage.features instead' )
527
552
pkgs = list_packages ('experimental' , local = True ).values ()
528
- return (sorted (pkg [ ' name' ] for pkg in pkgs if pkg [ 'installed' ] ),
529
- sorted (pkg [ ' name' ] for pkg in pkgs if not pkg [ 'installed' ] ))
553
+ return (sorted (pkg . name for pkg in pkgs if pkg . is_installed () ),
554
+ sorted (pkg . name for pkg in pkgs if not pkg . is_installed () ))
530
555
531
556
def package_manifest (package ):
532
557
"""
0 commit comments