Skip to content

Commit 906387d

Browse files
author
Release Manager
committed
Trac #31385: Improve interface of list_packages
(split out from #31013) ... representing package info using a new `PackageInfo` class, which provides also dict-like access, maintaining compatibility with the previous interface in this way. URL: https://trac.sagemath.org/31385 Reported by: mkoeppe Ticket author(s): Tobias Diez, Matthias Koeppe, John Palmieri Reviewer(s): Matthias Koeppe, John Palmieri, Tobias Diez
2 parents e786d48 + ebfb13c commit 906387d

File tree

4 files changed

+88
-56
lines changed

4 files changed

+88
-56
lines changed

src/bin/sage-list-packages

+6-6
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,16 @@ else:
9191
if WARN:
9292
print(WARN)
9393
if args['installed_only']:
94-
L = [pkg for pkg in L if pkg['installed']]
94+
L = [pkg for pkg in L if pkg.is_installed()]
9595
elif args['not_installed_only']:
96-
L = [pkg for pkg in L if not pkg['installed']]
96+
L = [pkg for pkg in L if not pkg.is_installed()]
9797

98-
L.sort(key=lambda pkg: pkg['name'])
98+
L.sort(key=lambda pkg: pkg.name)
9999

100100
# print (while getting rid of None in versions)
101101
for pkg in L:
102-
pkg['installed_version'] = pkg['installed_version'] or 'not_installed'
103-
pkg['remote_version'] = pkg['remote_version'] or '?'
104-
print(format_string.format(**pkg))
102+
pkg.installed_version = pkg.installed_version or 'not_installed'
103+
pkg.remote_version = pkg.remote_version or '?'
104+
print(format_string.format(**pkg._asdict()))
105105
if WARN:
106106
print(WARN)

src/sage/doctest/control.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,8 @@ def __init__(self, options, args):
402402
options.optional.discard('optional')
403403
from sage.misc.package import list_packages
404404
for pkg in list_packages('optional', local=True).values():
405-
if pkg['installed'] and pkg['installed_version'] == pkg['remote_version']:
406-
options.optional.add(pkg['name'])
405+
if pkg.is_installed() and pkg.installed_version == pkg.remote_version:
406+
options.optional.add(pkg.name)
407407

408408
from sage.features import package_systems
409409
options.optional.update(system.name for system in package_systems())

src/sage/misc/package.py

+79-47
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
# (at your option) any later version.
4141
# https://www.gnu.org/licenses/
4242
# ****************************************************************************
43+
from typing import Dict, List, NamedTuple, Optional, Union
4344

4445
import sage.env
4546

@@ -160,14 +161,13 @@ def pip_installed_packages(normalization=None):
160161
)
161162
stdout = proc.communicate()[0].decode()
162163

163-
def normalize(name):
164+
def normalize(name: str) -> str:
164165
if normalization is None:
165166
return name
166167
elif normalization == 'spkg':
167168
return name.lower().replace('-', '_').replace('.', '_')
168169
else:
169170
raise NotImplementedError(f'normalization {normalization} is not implemented')
170-
171171
try:
172172
return {normalize(package['name']): package['version']
173173
for package in json.loads(stdout)}
@@ -176,12 +176,56 @@ def normalize(name):
176176
# This may happen if pip is not correctly installed.
177177
return {}
178178

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]:
180225
r"""
181226
Return a dictionary of information about each package.
182227
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:
185229
186230
- ``'type'``: either ``'base``, ``'standard'``, ``'optional'``, or ``'experimental'``
187231
- ``'source'``: either ``'normal', ``'pip'``, or ``'script'``
@@ -213,54 +257,46 @@ def list_packages(*pkg_types, **opts):
213257
EXAMPLES::
214258
215259
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
218262
['alabaster',
219263
'arb',
220264
'babel',
221265
...
222266
'zn_poly']
223267
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
225269
'standard'
226-
sage: sage_conf_info['installed'] # optional - build
270+
sage: sage_conf_info.is_installed() # optional - build
227271
True
228-
sage: sage_conf_info['source'] # optional - build
272+
sage: sage_conf_info.source # optional - build
229273
'script'
230274
231275
sage: L = list_packages(pkg_sources=['pip'], local=True) # optional - build internet
232276
sage: bs4_info = L['beautifulsoup4'] # optional - build internet
233-
sage: bs4_info['type'] # optional - build internet
277+
sage: bs4_info.type # optional - build internet
234278
'optional'
235-
sage: bs4_info['source'] # optional - build internet
279+
sage: bs4_info.source # optional - build internet
236280
'pip'
237281
238282
Check the option ``exclude_pip``::
239283
240284
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']
242286
[]
243287
"""
244288
if not pkg_types:
245289
pkg_types = ('base', 'standard', 'optional', 'experimental')
246290
elif any(pkg_type not in ('base', 'standard', 'optional', 'experimental') for pkg_type in pkg_types):
247291
raise ValueError("Each pkg_type must be one of 'base', 'standard', 'optional', 'experimental'")
248292

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)
255293
if exclude_pip:
256294
pkg_sources = [s for s in pkg_sources if s != 'pip']
257-
if opts:
258-
raise ValueError("{} are not valid options".format(sorted(opts)))
259295

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)
262297
for p, v in installed_packages('pip' not in pkg_sources).items()}
263298

299+
# Add additional information based on Sage's package repository
264300
lp = []
265301
SAGE_PKGS = sage.env.SAGE_PKGS
266302
if not SAGE_PKGS:
@@ -289,33 +325,29 @@ def list_packages(*pkg_types, **opts):
289325
else:
290326
src = 'script'
291327

292-
pkg = pkgs.get(p, dict())
293-
pkgs[p] = pkg
294-
295328
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
297333
continue
298334

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':
307336
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)
309338
else:
310-
pkg['remote_version'] = None
311-
elif pkg['source'] == 'normal':
339+
remote_version = None
340+
elif src == 'normal':
312341
# If package-version.txt does not exist, that is an error
313342
# in the build system => we just propagate the exception
314343
package_filename = os.path.join(SAGE_PKGS, p, "package-version.txt")
315344
with open(package_filename) as f:
316-
pkg['remote_version'] = f.read().strip()
345+
remote_version = f.read().strip()
317346
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)
319351

320352
return pkgs
321353

@@ -444,7 +476,7 @@ def package_versions(package_type, local=False):
444476
sage: std['zn_poly'] # optional - build, random
445477
('0.9.p12', '0.9.p12')
446478
"""
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()}
448480

449481

450482
def standard_packages():
@@ -477,8 +509,8 @@ def standard_packages():
477509
'the functions standard_packages, optional_packages, experimental_packages'
478510
'are deprecated, use sage.features instead')
479511
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()))
482514

483515

484516
def optional_packages():
@@ -515,8 +547,8 @@ def optional_packages():
515547
'are deprecated, use sage.features instead')
516548
pkgs = list_packages('optional', local=True)
517549
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()))
520552

521553

522554
def experimental_packages():
@@ -547,8 +579,8 @@ def experimental_packages():
547579
'the functions standard_packages, optional_packages, experimental_packages'
548580
'are deprecated, use sage.features instead')
549581
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()))
552584

553585
def package_manifest(package):
554586
"""

src/sage_setup/optional_extension.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def is_package_installed_and_updated(pkg):
4848
# Might be an installed old-style package
4949
condition = is_package_installed(pkg)
5050
else:
51-
condition = (pkginfo["installed_version"] == pkginfo["remote_version"])
51+
condition = (pkginfo.installed_version == pkginfo.remote_version)
5252
return condition
5353

5454

0 commit comments

Comments
 (0)