Skip to content
This repository was archived by the owner on Jan 30, 2023. It is now read-only.

Commit 35642b3

Browse files
tobiasdiezMatthias Koeppe
authored and
Matthias Koeppe
committed
sage.misc.packages, sage-list-packages: Use new class PackageInfo
1 parent 13b4090 commit 35642b3

File tree

3 files changed

+79
-54
lines changed

3 files changed

+79
-54
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
@@ -363,8 +363,8 @@ def __init__(self, options, args):
363363
options.optional.discard('optional')
364364
from sage.misc.package import list_packages
365365
for pkg in list_packages('optional', local=True).values():
366-
if pkg['installed'] and pkg['installed_version'] == pkg['remote_version']:
367-
options.optional.add(pkg['name'])
366+
if pkg.is_installed() and pkg.installed_version == pkg.remote_version:
367+
options.optional.add(pkg.name)
368368

369369
from sage.features import package_systems
370370
options.optional.update(system.name for system in package_systems())

src/sage/misc/package.py

+71-46
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 Any, Dict, List, NamedTuple, Optional, Tuple, Union
4344

4445
import sage.env
4546

@@ -157,15 +158,13 @@ def pip_installed_packages(normalization=None):
157158
stderr=devnull,
158159
)
159160
stdout = proc.communicate()[0].decode()
160-
161-
def normalize(name):
161+
def normalize(name: str):
162162
if normalization is None:
163163
return name
164164
elif normalization == 'spkg':
165165
return name.lower().replace('-', '_').replace('.', '_')
166166
else:
167167
raise NotImplementedError(f'normalization {normalization} is not implemented')
168-
169168
try:
170169
return {normalize(package['name']): package['version']
171170
for package in json.loads(stdout)}
@@ -174,12 +173,50 @@ def normalize(name):
174173
# This may happen if pip is not correctly installed.
175174
return {}
176175

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]:
178216
r"""
179217
Return a dictionary of information about each package.
180218
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:
183220
184221
- ``'type'``: either ``'base``, ``'standard'``, ``'optional'``, or ``'experimental'``
185222
- ``'source'``: either ``'normal', ``'pip'``, or ``'script'``
@@ -219,46 +256,38 @@ def list_packages(*pkg_types, **opts):
219256
...
220257
'zn_poly']
221258
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
223260
'standard'
224-
sage: sage_conf_info['installed'] # optional - build
261+
sage: sage_conf_info.installed # optional - build
225262
True
226-
sage: sage_conf_info['source'] # optional - build
263+
sage: sage_conf_info.source # optional - build
227264
'script'
228265
229266
sage: L = list_packages(pkg_sources=['pip'], local=True) # optional - build internet
230267
sage: bs4_info = L['beautifulsoup4'] # optional - build internet
231-
sage: bs4_info['type'] # optional - build internet
268+
sage: bs4_info.type # optional - build internet
232269
'optional'
233-
sage: bs4_info['source'] # optional - build internet
270+
sage: bs4_info.source # optional - build internet
234271
'pip'
235272
236273
Check the option ``exclude_pip``::
237274
238275
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']
240277
[]
241278
"""
242279
if not pkg_types:
243280
pkg_types = ('base', 'standard', 'optional', 'experimental')
244281
elif any(pkg_type not in ('base', 'standard', 'optional', 'experimental') for pkg_type in pkg_types):
245282
raise ValueError("Each pkg_type must be one of 'base', 'standard', 'optional', 'experimental'")
246283

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)
253284
if exclude_pip:
254285
pkg_sources = [s for s in pkg_sources if s != 'pip']
255-
if opts:
256-
raise ValueError("{} are not valid options".format(sorted(opts)))
257286

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

290+
# Add additional information based on Sage's package repository
262291
lp = []
263292
SAGE_PKGS = sage.env.SAGE_PKGS
264293
if not SAGE_PKGS:
@@ -287,33 +316,29 @@ def list_packages(*pkg_types, **opts):
287316
else:
288317
src = 'script'
289318

290-
pkg = pkgs.get(p, dict())
291-
pkgs[p] = pkg
292-
293319
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
295324
continue
296325

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':
305327
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)
307329
else:
308-
pkg['remote_version'] = None
309-
elif pkg['source'] == 'normal':
330+
remote_version = None
331+
elif src == 'normal':
310332
# If package-version.txt does not exist, that is an error
311333
# in the build system => we just propagate the exception
312334
package_filename = os.path.join(SAGE_PKGS, p, "package-version.txt")
313335
with open(package_filename) as f:
314-
pkg['remote_version'] = f.read().strip()
336+
remote_version = f.read().strip()
315337
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)
317342

318343
return pkgs
319344

@@ -422,7 +447,7 @@ def package_versions(package_type, local=False):
422447
sage: std['zn_poly'] # optional - build, random
423448
('0.9.p12', '0.9.p12')
424449
"""
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()}
426451

427452

428453
def standard_packages():
@@ -455,8 +480,8 @@ def standard_packages():
455480
'the functions standard_packages, optional_packages, experimental_packages'
456481
'are deprecated, use sage.features instead')
457482
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()))
460485

461486

462487
def optional_packages():
@@ -493,8 +518,8 @@ def optional_packages():
493518
'are deprecated, use sage.features instead')
494519
pkgs = list_packages('optional', local=True)
495520
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()))
498523

499524

500525
def experimental_packages():
@@ -525,8 +550,8 @@ def experimental_packages():
525550
'the functions standard_packages, optional_packages, experimental_packages'
526551
'are deprecated, use sage.features instead')
527552
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()))
530555

531556
def package_manifest(package):
532557
"""

0 commit comments

Comments
 (0)