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