STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
_environment.py
Go to the documentation of this file.
1"""Which versions of the physics stack produced a result.
2
3Correlations and equations of state are not frozen. CoolProp revises a fluid's
4EOS between releases; ``fluids`` corrects a fitting coefficient. When a number
5that was right last month is wrong today and nothing in this repo changed, the
6stack version is the first thing you want and the last thing anyone records.
7
8So every run stamps this. It costs one dict and it is the difference between
9"the model drifted" and "CoolProp 7.3 revised the nitrogen EOS".
10
11A missing package reports ``None`` rather than raising: this is diagnostic
12plumbing, and it must never be the reason an import fails.
13"""
14
15from __future__ import annotations
16
17import functools
18import importlib.metadata
19import platform
20import sys
21
22#: Everything whose version can change a computed answer. Keys are the import
23#: name; values are the distribution name, where the two differ.
24_PHYSICS_STACK: dict[str, str] = {
25 "numpy": "numpy",
26 "scipy": "scipy",
27 "CoolProp": "CoolProp",
28 "fluids": "fluids",
29 "ht": "ht",
30}
31
32
33@functools.lru_cache(maxsize=1)
34def _stack_versions() -> tuple[tuple[str, str | None], ...]:
35 """The uncached scan, memoised. See :func:`stack_versions`."""
36 return tuple(_collect().items())
37
38
39def stack_versions() -> dict[str, str | None]:
40 """Versions of feedtwin and every dependency that can move a number.
41
42 Cached for the life of the process. Reading distribution metadata means
43 parsing an email-format file per package, which took about 4 ms -- half the
44 cost of a small steady solve, on every solve, for an answer that cannot
45 change while the interpreter is running. A fresh dict is returned each call
46 so a caller cannot mutate the cache.
47
48 Returns a mapping of package name to version string, with ``None`` for
49 anything not installed. ``python`` and ``platform`` are included because a
50 LAPACK or libm difference across platforms is a real, if rare, source of
51 divergence in a Newton solve.
52 """
53 return dict(_stack_versions())
54
55
56def _collect() -> dict[str, str | None]:
57 from feedtwin import __version__
58
59 versions: dict[str, str | None] = {
60 "feedtwin": __version__,
61 "python": platform.python_version(),
62 "platform": f"{platform.system().lower()}-{platform.machine()}",
63 }
64
65 for import_name, dist_name in _PHYSICS_STACK.items():
66 versions[import_name] = _version_of(import_name, dist_name)
67
68 return versions
69
70
71def _version_of(import_name: str, dist_name: str) -> str | None:
72 """Version of one dependency, or ``None`` if it is not installed.
73
74 Prefers the installed distribution's metadata over the module's own
75 ``__version__``: the metadata is what pip resolved, and a module attribute
76 can lag it. Falls back to the attribute for packages that ship no metadata,
77 and imports only as a last resort so this stays cheap on a warm interpreter.
78 """
79 try:
80 return importlib.metadata.version(dist_name)
81 except importlib.metadata.PackageNotFoundError:
82 pass
83
84 module = sys.modules.get(import_name)
85 if module is None:
86 return None
87
88 version = getattr(module, "__version__", None)
89 return str(version) if version is not None else None
dict[str, str|None] stack_versions()
str|None _version_of(str import_name, str dist_name)
dict[str, str|None] _collect()
tuple[tuple[str, str|None],...] _stack_versions()