passagemath-environment 10.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. passagemath_environment-10.4.1.data/scripts/sage +1140 -0
  2. passagemath_environment-10.4.1.data/scripts/sage-env +667 -0
  3. passagemath_environment-10.4.1.data/scripts/sage-num-threads.py +105 -0
  4. passagemath_environment-10.4.1.data/scripts/sage-python +2 -0
  5. passagemath_environment-10.4.1.data/scripts/sage-venv-config +42 -0
  6. passagemath_environment-10.4.1.data/scripts/sage-version.sh +9 -0
  7. passagemath_environment-10.4.1.dist-info/METADATA +76 -0
  8. passagemath_environment-10.4.1.dist-info/RECORD +70 -0
  9. passagemath_environment-10.4.1.dist-info/WHEEL +5 -0
  10. passagemath_environment-10.4.1.dist-info/top_level.txt +1 -0
  11. sage/all__sagemath_environment.py +4 -0
  12. sage/env.py +496 -0
  13. sage/features/__init__.py +981 -0
  14. sage/features/all.py +126 -0
  15. sage/features/bliss.py +85 -0
  16. sage/features/cddlib.py +38 -0
  17. sage/features/coxeter3.py +45 -0
  18. sage/features/csdp.py +83 -0
  19. sage/features/cython.py +38 -0
  20. sage/features/databases.py +302 -0
  21. sage/features/dvipng.py +40 -0
  22. sage/features/ecm.py +42 -0
  23. sage/features/ffmpeg.py +119 -0
  24. sage/features/four_ti_2.py +55 -0
  25. sage/features/fricas.py +66 -0
  26. sage/features/gap.py +86 -0
  27. sage/features/gfan.py +38 -0
  28. sage/features/giac.py +30 -0
  29. sage/features/graph_generators.py +171 -0
  30. sage/features/graphviz.py +117 -0
  31. sage/features/igraph.py +44 -0
  32. sage/features/imagemagick.py +138 -0
  33. sage/features/interfaces.py +256 -0
  34. sage/features/internet.py +65 -0
  35. sage/features/jmol.py +44 -0
  36. sage/features/join_feature.py +146 -0
  37. sage/features/kenzo.py +77 -0
  38. sage/features/latex.py +300 -0
  39. sage/features/latte.py +85 -0
  40. sage/features/lrs.py +164 -0
  41. sage/features/mcqd.py +45 -0
  42. sage/features/meataxe.py +46 -0
  43. sage/features/mip_backends.py +114 -0
  44. sage/features/msolve.py +68 -0
  45. sage/features/nauty.py +70 -0
  46. sage/features/normaliz.py +43 -0
  47. sage/features/palp.py +65 -0
  48. sage/features/pandoc.py +42 -0
  49. sage/features/pdf2svg.py +41 -0
  50. sage/features/phitigra.py +42 -0
  51. sage/features/pkg_systems.py +195 -0
  52. sage/features/polymake.py +43 -0
  53. sage/features/poppler.py +58 -0
  54. sage/features/rubiks.py +180 -0
  55. sage/features/sagemath.py +1205 -0
  56. sage/features/sat.py +103 -0
  57. sage/features/singular.py +48 -0
  58. sage/features/sirocco.py +45 -0
  59. sage/features/sphinx.py +71 -0
  60. sage/features/standard.py +38 -0
  61. sage/features/symengine_py.py +44 -0
  62. sage/features/tdlib.py +38 -0
  63. sage/features/threejs.py +75 -0
  64. sage/features/topcom.py +67 -0
  65. sage/misc/all__sagemath_environment.py +2 -0
  66. sage/misc/package.py +570 -0
  67. sage/misc/package_dir.py +621 -0
  68. sage/misc/temporary_file.py +546 -0
  69. sage/misc/viewer.py +369 -0
  70. sage/version.py +5 -0
sage/misc/package.py ADDED
@@ -0,0 +1,570 @@
1
+ # sage_setup: distribution = sagemath-environment
2
+ r"""
3
+ Listing Sage packages
4
+
5
+ This module can be used to see which Sage packages are installed
6
+ and which packages are available for installation.
7
+
8
+ For more information about creating Sage packages, see the "Packaging
9
+ Third-Party Code" section of the Sage Developer's Guide.
10
+
11
+ Actually installing the packages should be done via the command
12
+ line, using the following commands:
13
+
14
+ - ``sage -i PACKAGE_NAME`` -- install the given package
15
+
16
+ - ``sage -f PACKAGE_NAME`` -- re-install the given package, even if it
17
+ was already installed
18
+
19
+ To list the packages available, either use in a terminal one of ``sage
20
+ -standard``, ``sage -optional`` or ``sage -experimental``. Or the following
21
+ command inside Sage::
22
+
23
+ sage: from sage.misc.package import list_packages
24
+ sage: pkgs = list_packages(local=True) # optional - sage_spkg
25
+ sage: sorted(pkgs.keys()) # optional - sage_spkg, random
26
+ ['4ti2',
27
+ 'alabaster',
28
+ ...
29
+ 'zlib']
30
+
31
+ Functions
32
+ ---------
33
+ """
34
+
35
+ # ****************************************************************************
36
+ # This program is free software: you can redistribute it and/or modify
37
+ # it under the terms of the GNU General Public License as published by
38
+ # the Free Software Foundation, either version 2 of the License, or
39
+ # (at your option) any later version.
40
+ # https://www.gnu.org/licenses/
41
+ # ****************************************************************************
42
+ from typing import NamedTuple, Optional, Union
43
+
44
+ import sage.env
45
+
46
+ import json
47
+ import os
48
+ import subprocess
49
+ import sys
50
+ from pathlib import Path
51
+ from urllib.request import urlopen
52
+ from urllib.error import URLError
53
+ from ssl import create_default_context as default_context
54
+
55
+ DEFAULT_PYPI = 'https://pypi.org/pypi'
56
+
57
+
58
+ def pkgname_split(name):
59
+ r"""
60
+ Split a pkgname into a list of strings, 'name, version'.
61
+
62
+ For some packages, the version string might be empty.
63
+
64
+ EXAMPLES::
65
+
66
+ sage: from sage.misc.package import pkgname_split
67
+ sage: pkgname_split('hello_world-1.2')
68
+ ['hello_world', '1.2']
69
+ """
70
+ return (name.split('-', 1) + [''])[:2]
71
+
72
+
73
+ def pip_remote_version(pkg, pypi_url=DEFAULT_PYPI, ignore_URLError=False):
74
+ r"""
75
+ Return the version of this pip package available on PyPI.
76
+
77
+ INPUT:
78
+
79
+ - ``pkg`` -- the package
80
+
81
+ - ``pypi_url`` -- string (default: standard PyPI url) an optional Python
82
+ package repository to use
83
+
84
+ - ``ignore_URLError`` -- boolean (default: ``False``); if set to ``True`` then no
85
+ error is raised if the connection fails and the function returns ``None``
86
+
87
+ EXAMPLES:
88
+
89
+ The following test does fail if there is no TLS support (see e.g.
90
+ :issue:`19213`)::
91
+
92
+ sage: from sage.misc.package import pip_remote_version
93
+ sage: pip_remote_version('beautifulsoup4') # optional - internet # not tested
94
+ '...'
95
+
96
+ These tests are reliable since the tested package does not exist::
97
+
98
+ sage: nap = 'hey_this_is_NOT_a_python_package'
99
+ sage: pypi = 'http://this.is.not.pypi.com/'
100
+ sage: pip_remote_version(nap, pypi_url=pypi, ignore_URLError=True) # optional - internet
101
+ doctest:...: UserWarning: failed to fetch the version of
102
+ pkg='hey_this_is_NOT_a_python_package' at
103
+ http://this.is.not.pypi.com/.../json
104
+ sage: pip_remote_version(nap, pypi_url=pypi, ignore_URLError=False) # optional - internet
105
+ Traceback (most recent call last):
106
+ ...
107
+ HTTPError: HTTP Error 404: Not Found
108
+ """
109
+ url = '{pypi_url}/{pkg}/json'.format(pypi_url=pypi_url, pkg=pkg)
110
+
111
+ try:
112
+ f = urlopen(url, context=default_context())
113
+ text = f.read()
114
+ f.close()
115
+ except URLError:
116
+ if ignore_URLError:
117
+ import warnings
118
+ warnings.warn("failed to fetch the version of pkg={!r} at {}".format(pkg, url))
119
+ return
120
+ else:
121
+ raise
122
+
123
+ info = json.loads(text)
124
+ stable_releases = [v for v in info['releases'] if 'a' not in v and 'b' not in v]
125
+ return max(stable_releases)
126
+
127
+
128
+ def spkg_type(name):
129
+ r"""
130
+ Return the type of the Sage package with the given name.
131
+
132
+ INPUT:
133
+
134
+ - ``name`` -- string giving the subdirectory name of the package under
135
+ ``SAGE_PKGS``
136
+
137
+ EXAMPLES::
138
+
139
+ sage: from sage.misc.package import spkg_type
140
+ sage: spkg_type('pip') # optional - sage_spkg
141
+ 'standard'
142
+
143
+ OUTPUT:
144
+
145
+ The type as a string in ``('base', 'standard', 'optional', 'experimental')``.
146
+ If no ``SPKG`` exists with the given name (or the directory ``SAGE_PKGS`` is
147
+ not available), ``None`` is returned.
148
+ """
149
+ spkg_type = None
150
+ from sage.env import SAGE_PKGS
151
+ if not SAGE_PKGS:
152
+ return None
153
+ try:
154
+ f = open(os.path.join(SAGE_PKGS, name, "type"))
155
+ except OSError:
156
+ # Probably an empty directory => ignore
157
+ return None
158
+
159
+ with f:
160
+ spkg_type = f.read().strip()
161
+ return spkg_type
162
+
163
+
164
+ def pip_installed_packages(normalization=None):
165
+ r"""
166
+ Return a dictionary `name->version` of installed pip packages.
167
+
168
+ This command returns *all* pip-installed packages. Not only Sage packages.
169
+
170
+ INPUT:
171
+
172
+ - ``normalization`` -- (default: ``None``) according to which rule to
173
+ normalize the package name, either ``None`` (as is) or ``'spkg'`` (format
174
+ as in the Sage distribution in ``build/pkgs/``), i.e., lowercased and
175
+ dots and dashes replaced by underscores.
176
+
177
+ EXAMPLES::
178
+
179
+ sage: # optional - sage_spkg
180
+ sage: from sage.misc.package import pip_installed_packages
181
+ sage: d = pip_installed_packages()
182
+ sage: 'scipy' in d or 'SciPy' in d # needs scipy
183
+ True
184
+ sage: 'beautifulsoup4' in d # needs beautifulsoup4
185
+ True
186
+ sage: 'prompt-toolkit' in d or 'prompt_toolkit' in d # whether - or _ appears in the name depends on the setuptools version used for building the package
187
+ True
188
+ sage: d = pip_installed_packages(normalization='spkg')
189
+ sage: d['prompt_toolkit']
190
+ '...'
191
+ sage: d['scipy'] # needs scipy
192
+ '...'
193
+ """
194
+ with open(os.devnull, 'w') as devnull:
195
+ proc = subprocess.Popen(
196
+ [sys.executable, "-m", "pip", "list", "--no-index", "--format", "json"],
197
+ stdout=subprocess.PIPE,
198
+ stderr=devnull,
199
+ )
200
+ stdout = proc.communicate()[0].decode()
201
+
202
+ def normalize(name: str) -> str:
203
+ if normalization is None:
204
+ return name
205
+ elif normalization == 'spkg':
206
+ return name.lower().replace('-', '_').replace('.', '_')
207
+ else:
208
+ raise NotImplementedError(f'normalization {normalization} is not implemented')
209
+ try:
210
+ return {normalize(package['name']): package['version']
211
+ for package in json.loads(stdout)}
212
+ except json.decoder.JSONDecodeError:
213
+ # Something went wrong while parsing the output from pip.
214
+ # This may happen if pip is not correctly installed.
215
+ return {}
216
+
217
+
218
+ class PackageInfo(NamedTuple):
219
+ """Represents information about a package."""
220
+ name: str
221
+ type: Optional[str] = None
222
+ source: Optional[str] = None
223
+ installed_version: Optional[str] = None
224
+ remote_version: Optional[str] = None
225
+
226
+ def is_installed(self) -> bool:
227
+ r"""
228
+ Whether the package is installed in the system.
229
+ """
230
+ return self.installed_version is not None
231
+
232
+
233
+ def list_packages(*pkg_types: str, pkg_sources: list[str] = ['normal', 'pip', 'script'],
234
+ local: bool = False, ignore_URLError: bool = False, exclude_pip: bool = False) -> dict[str, PackageInfo]:
235
+ r"""
236
+ Return a dictionary of information about each package.
237
+
238
+ The keys are package names and values are named tuples with the following keys:
239
+
240
+ - ``'type'`` -- either ``'base``, ``'standard'``, ``'optional'``, or ``'experimental'``
241
+ - ``'source'`` -- either ``'normal', ``'pip'``, or ``'script'``
242
+ - ``'installed'`` -- boolean
243
+ - ``'installed_version'`` -- ``None`` or a string
244
+ - ``'remote_version'`` -- string
245
+
246
+ INPUT:
247
+
248
+ - ``pkg_types`` -- (optional) a sublist of ``'base``, ``'standard'``, ``'optional'``,
249
+ or ``'experimental'``. If provided, list only the packages with the
250
+ given type(s), otherwise list all packages.
251
+
252
+ - ``pkg_sources`` -- (optional) a sublist of ``'normal', ``'pip'``, or ``'script'``.
253
+ If provided, list only the packages with the given source(s), otherwise list all
254
+ packages.
255
+
256
+ - ``local`` -- boolean (default: ``False``); if set to ``True``, then do not
257
+ consult remote (PyPI) repositories for package versions (only applicable for
258
+ ``'pip'`` type)
259
+
260
+ - ``exclude_pip`` -- boolean (default: ``False``); if set to ``True``, then
261
+ pip packages are not considered. This is the same as removing ``'pip'``
262
+ from ``pkg_sources``
263
+
264
+ - ``ignore_URLError`` -- boolean (default: ``False``); if set to ``True``, then
265
+ connection errors will be ignored
266
+
267
+ EXAMPLES::
268
+
269
+ sage: # optional - sage_spkg
270
+ sage: from sage.misc.package import list_packages
271
+ sage: L = list_packages('standard')
272
+ sage: sorted(L.keys()) # random
273
+ ['alabaster',
274
+ 'babel',
275
+ ...
276
+ 'zlib']
277
+ sage: sage_conf_info = L['sage_conf']
278
+ sage: sage_conf_info.type
279
+ 'standard'
280
+ sage: sage_conf_info.is_installed()
281
+ True
282
+ sage: sage_conf_info.source
283
+ 'script'
284
+
285
+ sage: # optional - sage_spkg internet
286
+ sage: L = list_packages(pkg_sources=['pip'], local=True)
287
+ sage: bp_info = L['biopython']
288
+ sage: bp_info.type
289
+ 'optional'
290
+ sage: bp_info.source
291
+ 'pip'
292
+
293
+ Check the option ``exclude_pip``::
294
+
295
+ sage: [p for p, d in list_packages('optional', exclude_pip=True).items() # optional - sage_spkg
296
+ ....: if d.source == 'pip']
297
+ []
298
+ """
299
+ if not pkg_types:
300
+ pkg_types = ('base', 'standard', 'optional', 'experimental')
301
+ elif any(pkg_type not in ('base', 'standard', 'optional', 'experimental') for pkg_type in pkg_types):
302
+ raise ValueError("Each pkg_type must be one of 'base', 'standard', 'optional', 'experimental'")
303
+
304
+ if exclude_pip:
305
+ pkg_sources = [s for s in pkg_sources if s != 'pip']
306
+
307
+ pkgs = {p: PackageInfo(name=p, installed_version=v)
308
+ for p, v in installed_packages('pip' not in pkg_sources).items()}
309
+
310
+ # Add additional information based on Sage's package repository
311
+ lp = []
312
+ SAGE_PKGS = sage.env.SAGE_PKGS
313
+ if not SAGE_PKGS:
314
+ return pkgs
315
+
316
+ try:
317
+ lp = os.listdir(SAGE_PKGS)
318
+ except FileNotFoundError:
319
+ return pkgs
320
+
321
+ for p in lp:
322
+
323
+ typ = spkg_type(p)
324
+ if not typ:
325
+ continue
326
+
327
+ if os.path.isfile(os.path.join(SAGE_PKGS, p, "requirements.txt")):
328
+ src = 'pip'
329
+ elif os.path.isfile(os.path.join(SAGE_PKGS, p, "checksums.ini")):
330
+ src = 'normal'
331
+ else:
332
+ src = 'script'
333
+
334
+ if typ not in pkg_types or src not in pkg_sources:
335
+ try:
336
+ del pkgs[p]
337
+ except KeyError:
338
+ pass
339
+ continue
340
+
341
+ if src == 'pip':
342
+ if not local:
343
+ remote_version = pip_remote_version(p, ignore_URLError=ignore_URLError)
344
+ else:
345
+ remote_version = None
346
+ elif src == 'normal':
347
+ # If package-version.txt does not exist, that is an error
348
+ # in the build system => we just propagate the exception
349
+ package_filename = os.path.join(SAGE_PKGS, p, "package-version.txt")
350
+ with open(package_filename) as f:
351
+ remote_version = f.read().strip()
352
+ else:
353
+ remote_version = None
354
+
355
+ pkg = pkgs.get(p, PackageInfo(name=p))
356
+ pkgs[p] = PackageInfo(p, typ, src, pkg.installed_version, remote_version)
357
+
358
+ return pkgs
359
+
360
+
361
+ def _spkg_inst_dirs():
362
+ """
363
+ Generator for the installation manifest directories as resolved paths.
364
+
365
+ It yields first ``SAGE_LOCAL_SPKG_INST``, then ``SAGE_VENV_SPKG_INST``,
366
+ if defined; but it both resolve to the same directory, it only yields
367
+ one element.
368
+
369
+ EXAMPLES::
370
+
371
+ sage: from sage.misc.package import _spkg_inst_dirs
372
+ sage: list(_spkg_inst_dirs())
373
+ [...]
374
+ """
375
+ last_inst_dir = None
376
+ for inst_dir in (sage.env.SAGE_LOCAL_SPKG_INST, sage.env.SAGE_VENV_SPKG_INST):
377
+ if inst_dir:
378
+ inst_dir = Path(inst_dir).resolve()
379
+ if inst_dir.is_dir() and inst_dir != last_inst_dir:
380
+ yield inst_dir
381
+ last_inst_dir = inst_dir
382
+
383
+
384
+ def installed_packages(exclude_pip=True):
385
+ """
386
+ Return a dictionary of all installed packages, with version numbers.
387
+
388
+ INPUT:
389
+
390
+ - ``exclude_pip`` -- boolean (default: ``True``); whether "pip" packages
391
+ are excluded from the list
392
+
393
+ EXAMPLES:
394
+
395
+ Below we test for a standard package without ``spkg-configure.m4`` script
396
+ that should be installed in ``SAGE_LOCAL``. When Sage is installed by
397
+ the Sage distribution (indicated by feature ``sage_spkg``), we should have
398
+ the installation record for this package. (We do not test for installation
399
+ records of Python packages. Our ``SAGE_VENV`` is not necessarily the
400
+ main Sage venv; it could be a user-created venv or a venv created by tox.)::
401
+
402
+ sage: # optional - sage_spkg
403
+ sage: from sage.misc.package import installed_packages
404
+ sage: sorted(installed_packages().keys())
405
+ [...'gnulib', ...]
406
+ sage: installed_packages()['gnulib'] # random
407
+ 'f9b39c4e337f1dc0dd07c4f3985c476fb875d799'
408
+
409
+ .. SEEALSO::
410
+
411
+ :func:`sage.misc.package.list_packages`
412
+ """
413
+ installed = {}
414
+ if not exclude_pip:
415
+ installed.update(pip_installed_packages(normalization='spkg'))
416
+ # Sage packages should override pip packages (Issue #23997)
417
+
418
+ for inst_dir in _spkg_inst_dirs():
419
+ try:
420
+ lp = os.listdir(inst_dir)
421
+ installed.update(pkgname_split(pkgname) for pkgname in lp
422
+ if not pkgname.startswith('.'))
423
+ except FileNotFoundError:
424
+ pass
425
+ return installed
426
+
427
+
428
+ def is_package_installed(package, exclude_pip=True):
429
+ """
430
+ Return whether (any version of) ``package`` is installed.
431
+
432
+ INPUT:
433
+
434
+ - ``package`` -- the name of the package
435
+
436
+ - ``exclude_pip`` -- boolean (default: ``True``); whether to consider pip
437
+ type packages
438
+
439
+ EXAMPLES::
440
+
441
+ sage: from sage.misc.package import is_package_installed
442
+ sage: is_package_installed('gnulib') # optional - sage_spkg
443
+ True
444
+
445
+ Giving just the beginning of the package name is not good enough::
446
+
447
+ sage: is_package_installed('conway_poly') # optional - sage_spkg
448
+ False
449
+
450
+ Otherwise, installing "pillow" would cause this function to think
451
+ that "pil" is installed, for example.
452
+
453
+ .. NOTE::
454
+
455
+ Do not use this function to check whether you can use a feature from an
456
+ external library. This only checks whether something was installed with
457
+ ``sage -i`` but it may have been installed by other means (for example
458
+ if this copy of Sage has been installed as part of a distribution.)
459
+ Use the framework provided by :mod:`sage.features` to check
460
+ whether a library is installed and functional.
461
+ """
462
+ return any(p == package for p in installed_packages(exclude_pip))
463
+
464
+
465
+ def is_package_installed_and_updated(package: str) -> bool:
466
+ r"""
467
+ Return whether the given package is installed and up-to-date.
468
+
469
+ INPUT:
470
+
471
+ - ``package`` -- the name of the package
472
+
473
+ EXAMPLES::
474
+
475
+ sage: from sage.misc.package import is_package_installed_and_updated
476
+ sage: is_package_installed_and_updated("alabaster") # optional - build, random
477
+ False
478
+ """
479
+ try:
480
+ all_packages = list_packages(local=True)
481
+ pkginfo = all_packages[package]
482
+ return pkginfo.installed_version == pkginfo.remote_version
483
+ except KeyError:
484
+ # Might be an installed old-style package
485
+ return is_package_installed(package)
486
+
487
+
488
+ def package_versions(package_type, local=False):
489
+ r"""
490
+ Return version information for each Sage package.
491
+
492
+ INPUT:
493
+
494
+ - ``package_type`` -- string; one of ``'standard'``, ``'optional'`` or
495
+ ``'experimental'``
496
+
497
+ - ``local`` -- boolean (default: ``False``); only query local data (no internet needed)
498
+
499
+ For packages of the given type, return a dictionary whose entries
500
+ are of the form ``'package': (installed, latest)``, where
501
+ ``installed`` is the installed version (or ``None`` if not
502
+ installed) and ``latest`` is the latest available version. If the
503
+ package has a directory in ``SAGE_ROOT/build/pkgs/``, then
504
+ ``latest`` is determined by the file ``package-version.txt`` in
505
+ that directory. If ``local`` is ``False``, then Sage's servers are
506
+ queried for package information.
507
+
508
+ .. SEEALSO:: :func:`sage.misc.package.list_packages`
509
+
510
+ EXAMPLES::
511
+
512
+ sage: # optional - sage_spkg
513
+ sage: from sage.misc.package import package_versions
514
+ sage: std = package_versions('standard', local=True)
515
+ sage: 'gap' in std
516
+ True
517
+ sage: std['zlib'] # random
518
+ ('1.2.11.p0', '1.2.11.p0')
519
+ """
520
+ return {pkg.name: (pkg.installed_version, pkg.remote_version) for pkg in list_packages(package_type, local=local).values()}
521
+
522
+
523
+ def package_manifest(package):
524
+ """
525
+ Return the manifest for ``package``.
526
+
527
+ INPUT:
528
+
529
+ - ``package`` -- package name
530
+
531
+ The manifest is written in the file
532
+ ``SAGE_SPKG_INST/package-VERSION``. It is a JSON file containing a
533
+ dictionary with the package name, version, installation date, list
534
+ of installed files, etc.
535
+
536
+ EXAMPLES::
537
+
538
+ sage: # optional - sage_spkg
539
+ sage: from sage.misc.package import package_manifest
540
+ sage: manifest = package_manifest('gnulib')
541
+ sage: manifest['package_name'] == 'gnulib'
542
+ True
543
+ sage: 'files' in manifest
544
+ True
545
+
546
+ Test a nonexistent package::
547
+
548
+ sage: package_manifest('dummy-package') # optional - sage_spkg
549
+ Traceback (most recent call last):
550
+ ...
551
+ KeyError: 'dummy-package'
552
+ """
553
+ version = installed_packages()[package]
554
+ for inst_dir in _spkg_inst_dirs():
555
+ stamp_file = os.path.join(inst_dir,
556
+ '{}-{}'.format(package, version))
557
+ try:
558
+ with open(stamp_file) as f:
559
+ return json.load(f)
560
+ except FileNotFoundError:
561
+ pass
562
+ raise RuntimeError('package manifest directory changed at runtime')
563
+
564
+
565
+ # PackageNotFoundError used to be an exception class.
566
+ # It was deprecated in #30607 and removed afterwards.
567
+ # User code can continue to use PackageNotFoundError in
568
+ # try...except statements using this definition, which
569
+ # catches no exception.
570
+ PackageNotFoundError = ()