matlab-versions 0.1.0__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.
- matlab_versions/__init__.py +319 -0
- matlab_versions/__main__.py +84 -0
- matlab_versions/py.typed +0 -0
- matlab_versions/versions.json +919 -0
- matlab_versions-0.1.0.dist-info/METADATA +178 -0
- matlab_versions-0.1.0.dist-info/RECORD +9 -0
- matlab_versions-0.1.0.dist-info/WHEEL +4 -0
- matlab_versions-0.1.0.dist-info/entry_points.txt +2 -0
- matlab_versions-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Translate between the identifiers of a MATLAB release.
|
|
2
|
+
|
|
3
|
+
Every MATLAB release has several numbers, and they are easy to mix up:
|
|
4
|
+
|
|
5
|
+
========= ============== =============== =============== ===============
|
|
6
|
+
Release MATLAB version Runtime version Runtime folder FlexLM (lmstat)
|
|
7
|
+
========= ============== =============== =============== ===============
|
|
8
|
+
R2016a 9.0 9.0.1 v901 v35
|
|
9
|
+
R2021a 9.10 9.10 v910 v45
|
|
10
|
+
R2024a 24.1 24.1 R2024a v51
|
|
11
|
+
========= ============== =============== =============== ===============
|
|
12
|
+
|
|
13
|
+
* the **MATLAB version** is what ``version()`` and ``VersionInfo.xml`` report;
|
|
14
|
+
* the **MATLAB Runtime version** is what compiled applications need, installed
|
|
15
|
+
under a folder named ``v910`` (up to R2022a) or ``R2024a`` (from R2022b);
|
|
16
|
+
* the **licence version** is what a client checks out from a FlexLM licence
|
|
17
|
+
server, printed by ``lmutil lmstat -a`` on every checkout line as ``(v51)``.
|
|
18
|
+
|
|
19
|
+
The bundled table (``versions.json``) covers R2006a onwards and records, for
|
|
20
|
+
each value, how well it is supported and where it comes from. Releases newer
|
|
21
|
+
than the table are computed from the current rules and marked
|
|
22
|
+
``"extrapolated"``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from importlib.resources import files
|
|
31
|
+
from typing import Dict, Iterable, Iterator, List, Mapping, Optional, Tuple, Union
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"Checkout",
|
|
35
|
+
"Release",
|
|
36
|
+
"data",
|
|
37
|
+
"from_license",
|
|
38
|
+
"from_matlab",
|
|
39
|
+
"from_runtime",
|
|
40
|
+
"lookup",
|
|
41
|
+
"parse_checkout",
|
|
42
|
+
"parse_lmstat",
|
|
43
|
+
"release",
|
|
44
|
+
"releases",
|
|
45
|
+
]
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
|
|
48
|
+
#: Oldest licence version the +1 rule holds for (R2007a).
|
|
49
|
+
FIRST_LICENSE = 17
|
|
50
|
+
|
|
51
|
+
_NAME_RE = re.compile(r"^\s*R?(?P<year>\d{4})(?P<half>[ab])\s*$", re.IGNORECASE)
|
|
52
|
+
_LICENSE_RE = re.compile(r"^\s*v?(?P<major>\d+)(?:\.\d+)?\s*$", re.IGNORECASE)
|
|
53
|
+
_DOTTED_RE = re.compile(r"^\s*(?P<version>\d+(?:\.\d+)+)(?:\s.*)?$")
|
|
54
|
+
_FOLDER_RE = re.compile(r"^\s*v(?P<digits>\d{2,4})\s*$", re.IGNORECASE)
|
|
55
|
+
_FEATURE_RE = re.compile(r"^Users of\s+(?P<feature>\S+?):", re.IGNORECASE)
|
|
56
|
+
_CHECKOUT_RE = re.compile(
|
|
57
|
+
r"^\s+(?P<user>\S+)\s+(?P<host>\S+)\s+(?:(?P<display>.*?)\s+)?"
|
|
58
|
+
r"\(v(?P<version>\d+)(?:\.\d+)?\)\s+"
|
|
59
|
+
r"\((?P<server>[^/\s]+)/(?P<port>\d+)\s+(?P<handle>\d+)\)"
|
|
60
|
+
r"(?:,\s+start\s+(?P<start>.+?))?"
|
|
61
|
+
r"(?:,\s+PID:\s*(?P<pid>\d+))?\s*$"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class Release:
|
|
67
|
+
"""One MATLAB release and its identifiers. Missing values are ``None``."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
"""Release name, e.g. ``"R2024a"``."""
|
|
71
|
+
matlab: Optional[str]
|
|
72
|
+
"""MATLAB version as ``version()`` reports it, major.minor only, e.g. ``"24.1"``."""
|
|
73
|
+
runtime: Optional[str]
|
|
74
|
+
"""MATLAB Runtime version, e.g. ``"24.1"`` (``"9.0.1"`` for R2016a)."""
|
|
75
|
+
license: Optional[int]
|
|
76
|
+
"""FlexLM licence version, e.g. ``51`` (shown by lmstat as ``(v51)``)."""
|
|
77
|
+
evidence: Mapping[str, str] = field(default_factory=dict, compare=False, hash=False)
|
|
78
|
+
"""Per field: ``"observed"``, ``"documented"`` or ``"extrapolated"`` (see ``data()``)."""
|
|
79
|
+
sources: Mapping[str, Tuple[str, ...]] = field(default_factory=dict, compare=False, hash=False)
|
|
80
|
+
"""Per field: keys into ``data()["sources"]``."""
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def year(self) -> int:
|
|
84
|
+
return int(self.name[1:5])
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def half(self) -> str:
|
|
88
|
+
return self.name[5]
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def runtime_folder(self) -> Optional[str]:
|
|
92
|
+
"""Folder the MATLAB Runtime installs under: ``"v910"`` up to R2022a, ``"R2022b"`` after."""
|
|
93
|
+
if self.runtime is None:
|
|
94
|
+
return None
|
|
95
|
+
if (self.year, self.half) >= (2022, "b"):
|
|
96
|
+
return self.name
|
|
97
|
+
return "v" + self.runtime.replace(".", "")
|
|
98
|
+
|
|
99
|
+
def __str__(self) -> str:
|
|
100
|
+
return self.name
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class Checkout:
|
|
105
|
+
"""One checkout line from ``lmutil lmstat -a``."""
|
|
106
|
+
|
|
107
|
+
user: str
|
|
108
|
+
host: str
|
|
109
|
+
display: Optional[str]
|
|
110
|
+
version: int
|
|
111
|
+
"""Licence version the client checked out, e.g. ``51``."""
|
|
112
|
+
server: str
|
|
113
|
+
port: int
|
|
114
|
+
handle: int
|
|
115
|
+
start: Optional[str]
|
|
116
|
+
pid: Optional[int]
|
|
117
|
+
feature: Optional[str] = None
|
|
118
|
+
"""The ``Users of <feature>:`` section the line was in (only from :func:`parse_lmstat`)."""
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def release(self) -> Optional[Release]:
|
|
122
|
+
"""The client's MATLAB release, or ``None`` for licence versions before R2007a."""
|
|
123
|
+
try:
|
|
124
|
+
return from_license(self.version)
|
|
125
|
+
except ValueError:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
_DATA: Optional[dict] = None
|
|
130
|
+
_BY_NAME: Dict[str, Release] = {}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def data() -> dict:
|
|
134
|
+
"""The bundled table: description, rules, caveats, evidence levels, sources and releases."""
|
|
135
|
+
global _DATA
|
|
136
|
+
if _DATA is None:
|
|
137
|
+
text = files(__package__).joinpath("versions.json").read_text(encoding="utf-8")
|
|
138
|
+
_DATA = json.loads(text)
|
|
139
|
+
for row in _DATA["releases"]:
|
|
140
|
+
rel = Release(
|
|
141
|
+
name=row["release"],
|
|
142
|
+
matlab=row.get("matlab"),
|
|
143
|
+
runtime=row.get("runtime"),
|
|
144
|
+
license=row.get("license"),
|
|
145
|
+
evidence=dict(row.get("evidence", {})),
|
|
146
|
+
sources={k: tuple(v) for k, v in row.get("sources", {}).items()},
|
|
147
|
+
)
|
|
148
|
+
_BY_NAME[rel.name] = rel
|
|
149
|
+
return _DATA
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def releases() -> Tuple[Release, ...]:
|
|
153
|
+
"""All releases in the bundled table, oldest first."""
|
|
154
|
+
data()
|
|
155
|
+
return tuple(sorted(_BY_NAME.values(), key=lambda r: (r.year, r.half)))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _newest() -> Release:
|
|
159
|
+
return releases()[-1]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _is_newer(year: int, half: str) -> bool:
|
|
163
|
+
newest = _newest()
|
|
164
|
+
return (year, half) > (newest.year, newest.half)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _extrapolate(year: int, half: str) -> Release:
|
|
168
|
+
"""A release newer than the table, from the rules that hold since R2023b."""
|
|
169
|
+
ver = f"{year % 100}.{1 if half == 'a' else 2}"
|
|
170
|
+
lic = 2 * (year - 2008) + 19 + (half == "b")
|
|
171
|
+
return Release(
|
|
172
|
+
name=f"R{year}{half}", matlab=ver, runtime=ver, license=lic,
|
|
173
|
+
evidence={"matlab": "extrapolated", "runtime": "extrapolated", "license": "extrapolated"},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def release(name: str) -> Release:
|
|
178
|
+
"""Release by name: ``"R2024a"``, ``"2024a"`` or ``"r2024A"``."""
|
|
179
|
+
m = _NAME_RE.match(str(name))
|
|
180
|
+
if not m:
|
|
181
|
+
raise ValueError(f"not a release name like 'R2024a': {name!r}")
|
|
182
|
+
year, half = int(m.group("year")), m.group("half").lower()
|
|
183
|
+
data()
|
|
184
|
+
found = _BY_NAME.get(f"R{year}{half}")
|
|
185
|
+
if found:
|
|
186
|
+
return found
|
|
187
|
+
if _is_newer(year, half):
|
|
188
|
+
return _extrapolate(year, half)
|
|
189
|
+
raise ValueError(f"R{year}{half} is not covered (the table starts at {releases()[0].name})")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def from_license(ver: Union[int, str]) -> Release:
|
|
193
|
+
"""Release whose clients check out this licence version: ``51``, ``"v51"``, ``"v51.0"``."""
|
|
194
|
+
if isinstance(ver, bool):
|
|
195
|
+
raise TypeError("licence version must be an int or str")
|
|
196
|
+
if isinstance(ver, str):
|
|
197
|
+
m = _LICENSE_RE.match(ver)
|
|
198
|
+
if not m:
|
|
199
|
+
raise ValueError(f"not a licence version: {ver!r}")
|
|
200
|
+
ver = int(m.group("major"))
|
|
201
|
+
if ver < FIRST_LICENSE:
|
|
202
|
+
raise ValueError(f"licence version {ver} predates R2007a, where the numbering differs")
|
|
203
|
+
for rel in releases():
|
|
204
|
+
if rel.license == ver:
|
|
205
|
+
return rel
|
|
206
|
+
steps = ver - 19
|
|
207
|
+
return _extrapolate(2008 + steps // 2, "ab"[steps % 2])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _two_part(version: str) -> str:
|
|
211
|
+
return ".".join(version.split(".")[:2])
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def from_matlab(ver: str) -> Release:
|
|
215
|
+
"""Release for a MATLAB version: ``"24.1"``, ``"9.10"`` or a full ``version()`` string
|
|
216
|
+
such as ``"9.10.0.1739362 (R2021a)"``."""
|
|
217
|
+
m = _DOTTED_RE.match(str(ver))
|
|
218
|
+
if not m:
|
|
219
|
+
raise ValueError(f"not a MATLAB version like '24.1': {ver!r}")
|
|
220
|
+
wanted = _two_part(m.group("version"))
|
|
221
|
+
for rel in releases():
|
|
222
|
+
if rel.matlab == wanted:
|
|
223
|
+
return rel
|
|
224
|
+
major, minor = (int(x) for x in wanted.split("."))
|
|
225
|
+
if major >= 23 and minor in (1, 2) and _is_newer(2000 + major, "ab"[minor - 1]):
|
|
226
|
+
return _extrapolate(2000 + major, "ab"[minor - 1])
|
|
227
|
+
raise ValueError(f"unknown MATLAB version: {ver!r}")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def from_runtime(ver: str) -> Release:
|
|
231
|
+
"""Release for a MATLAB Runtime version or install folder:
|
|
232
|
+
``"9.14"``, ``"9.0.1"``, ``"v914"``, ``"v901"`` or ``"R2023b"``."""
|
|
233
|
+
text = str(ver).strip()
|
|
234
|
+
if _NAME_RE.match(text):
|
|
235
|
+
rel = release(text)
|
|
236
|
+
if rel.runtime is None:
|
|
237
|
+
raise ValueError(f"no MATLAB Runtime version is known for {rel.name}")
|
|
238
|
+
return rel
|
|
239
|
+
if _FOLDER_RE.match(text):
|
|
240
|
+
for rel in releases():
|
|
241
|
+
if rel.runtime_folder and rel.runtime_folder.lower() == text.lower():
|
|
242
|
+
return rel
|
|
243
|
+
raise ValueError(f"unknown MATLAB Runtime folder: {ver!r}")
|
|
244
|
+
m = _DOTTED_RE.match(text)
|
|
245
|
+
if not m:
|
|
246
|
+
raise ValueError(f"not a MATLAB Runtime version like '9.14' or 'v914': {ver!r}")
|
|
247
|
+
full = m.group("version")
|
|
248
|
+
for candidate in (full, _two_part(full)):
|
|
249
|
+
for rel in releases():
|
|
250
|
+
if rel.runtime == candidate:
|
|
251
|
+
return rel
|
|
252
|
+
major, minor = (int(x) for x in _two_part(full).split("."))
|
|
253
|
+
if major >= 23 and minor in (1, 2) and _is_newer(2000 + major, "ab"[minor - 1]):
|
|
254
|
+
return _extrapolate(2000 + major, "ab"[minor - 1])
|
|
255
|
+
raise ValueError(f"unknown MATLAB Runtime version: {ver!r}")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def lookup(term: Union[int, str]) -> List[Tuple[str, Release]]:
|
|
259
|
+
"""Every reading of ``term``, as ``(kind, release)`` pairs.
|
|
260
|
+
|
|
261
|
+
``kind`` is ``"name"``, ``"matlab"``, ``"runtime"`` or ``"license"``. Some
|
|
262
|
+
terms have more than one reading: ``"9.0"`` is MATLAB R2016a but MATLAB
|
|
263
|
+
Runtime R2015b. An empty list means nothing matched.
|
|
264
|
+
"""
|
|
265
|
+
text = str(term).strip()
|
|
266
|
+
if _NAME_RE.match(text):
|
|
267
|
+
try:
|
|
268
|
+
return [("name", release(text))]
|
|
269
|
+
except ValueError:
|
|
270
|
+
return []
|
|
271
|
+
if "." in text and not text.lower().startswith("v"):
|
|
272
|
+
readings = (("matlab", from_matlab), ("runtime", from_runtime))
|
|
273
|
+
elif _FOLDER_RE.match(text) and len(text.strip()) >= 4:
|
|
274
|
+
# three or more digits ("v914", "v901") can only be a Runtime folder
|
|
275
|
+
readings = (("runtime", from_runtime),)
|
|
276
|
+
else:
|
|
277
|
+
# "51", "v51", "v51.0" are licence versions; "v91" could also be a Runtime folder
|
|
278
|
+
readings = (("license", from_license), ("runtime", from_runtime))
|
|
279
|
+
found = []
|
|
280
|
+
for kind, fn in readings:
|
|
281
|
+
try:
|
|
282
|
+
found.append((kind, fn(text)))
|
|
283
|
+
except ValueError:
|
|
284
|
+
pass
|
|
285
|
+
return found
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def parse_checkout(line: str, feature: Optional[str] = None) -> Optional[Checkout]:
|
|
289
|
+
"""Parse one lmstat checkout line; ``None`` if it is not one."""
|
|
290
|
+
m = _CHECKOUT_RE.match(line.rstrip("\r\n"))
|
|
291
|
+
if not m:
|
|
292
|
+
return None
|
|
293
|
+
pid = m.group("pid")
|
|
294
|
+
return Checkout(
|
|
295
|
+
user=m.group("user"),
|
|
296
|
+
host=m.group("host"),
|
|
297
|
+
display=m.group("display") or None,
|
|
298
|
+
version=int(m.group("version")),
|
|
299
|
+
server=m.group("server"),
|
|
300
|
+
port=int(m.group("port")),
|
|
301
|
+
handle=int(m.group("handle")),
|
|
302
|
+
start=m.group("start"),
|
|
303
|
+
pid=int(pid) if pid else None,
|
|
304
|
+
feature=feature,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def parse_lmstat(text: Union[str, Iterable[str]]) -> Iterator[Checkout]:
|
|
309
|
+
"""Yield every checkout in ``lmutil lmstat -a`` output, tagged with its feature."""
|
|
310
|
+
lines = text.splitlines() if isinstance(text, str) else text
|
|
311
|
+
feature = None
|
|
312
|
+
for line in lines:
|
|
313
|
+
m = _FEATURE_RE.match(line)
|
|
314
|
+
if m:
|
|
315
|
+
feature = m.group("feature")
|
|
316
|
+
continue
|
|
317
|
+
checkout = parse_checkout(line, feature)
|
|
318
|
+
if checkout is not None:
|
|
319
|
+
yield checkout
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Command line: ``matlab-versions R2024a 9.14 v51 v914``, ``lmstat -a | matlab-versions``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from . import Release, __version__, data, lookup, parse_lmstat, releases
|
|
12
|
+
|
|
13
|
+
_KIND = {"name": "release", "matlab": "MATLAB version", "runtime": "Runtime", "license": "licence version"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _describe(rel: Release) -> str:
|
|
17
|
+
def cell(value, key):
|
|
18
|
+
if value is None:
|
|
19
|
+
return "-"
|
|
20
|
+
return f"{value}*" if rel.evidence.get(key) == "extrapolated" else str(value)
|
|
21
|
+
|
|
22
|
+
runtime = cell(rel.runtime, "runtime")
|
|
23
|
+
if rel.runtime_folder:
|
|
24
|
+
runtime += f" ({rel.runtime_folder})"
|
|
25
|
+
license = cell(f"v{rel.license}" if rel.license is not None else None, "license")
|
|
26
|
+
return f"{rel.name}\tmatlab {cell(rel.matlab, 'matlab')}\truntime {runtime}\tlicense {license}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
30
|
+
parser = argparse.ArgumentParser(
|
|
31
|
+
prog="matlab-versions",
|
|
32
|
+
description="Translate between MATLAB release names (R2024a), MATLAB versions (24.1), "
|
|
33
|
+
"MATLAB Runtime versions and folders (9.14, v914) and FlexLM licence versions (lmstat '(v51)'). "
|
|
34
|
+
"With no arguments, reads 'lmutil lmstat -a' output from stdin and lists each checkout's release. "
|
|
35
|
+
"Values marked * are extrapolated.",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument("terms", nargs="*", metavar="TERM", help="e.g. R2024a, 24.1, 9.14, v914, v51, 51")
|
|
38
|
+
group = parser.add_mutually_exclusive_group()
|
|
39
|
+
group.add_argument("--table", action="store_true", help="print the bundled table")
|
|
40
|
+
group.add_argument("--json", action="store_true", help="print the bundled table as JSON, with sources")
|
|
41
|
+
parser.add_argument("--summary", action="store_true", help="with stdin: count checkouts per feature and release")
|
|
42
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
43
|
+
args = parser.parse_args(argv)
|
|
44
|
+
|
|
45
|
+
if args.json:
|
|
46
|
+
json.dump(data(), sys.stdout, indent=2, ensure_ascii=False)
|
|
47
|
+
print()
|
|
48
|
+
return 0
|
|
49
|
+
if args.table:
|
|
50
|
+
for rel in releases():
|
|
51
|
+
print(_describe(rel))
|
|
52
|
+
return 0
|
|
53
|
+
if args.terms:
|
|
54
|
+
status = 0
|
|
55
|
+
for term in args.terms:
|
|
56
|
+
matches = lookup(term)
|
|
57
|
+
if not matches:
|
|
58
|
+
print(f"matlab-versions: nothing matches {term!r}", file=sys.stderr)
|
|
59
|
+
status = 2
|
|
60
|
+
# "9.14" reads as both MATLAB and Runtime version of R2023a: one line, both kinds
|
|
61
|
+
kinds_by_release = {}
|
|
62
|
+
for kind, rel in matches:
|
|
63
|
+
kinds_by_release.setdefault(rel, []).append(_KIND[kind])
|
|
64
|
+
for rel, kinds in kinds_by_release.items():
|
|
65
|
+
print(f"{term} ({', '.join(kinds)})\t{_describe(rel)}")
|
|
66
|
+
return status
|
|
67
|
+
|
|
68
|
+
if sys.stdin.isatty():
|
|
69
|
+
parser.print_usage(sys.stderr)
|
|
70
|
+
return 2
|
|
71
|
+
checkouts = list(parse_lmstat(sys.stdin))
|
|
72
|
+
if args.summary:
|
|
73
|
+
counts = Counter((c.feature or "?", c.release.name if c.release else f"v{c.version}") for c in checkouts)
|
|
74
|
+
for (feature, name), n in sorted(counts.items()):
|
|
75
|
+
print(f"{feature}\t{name}\t{n}")
|
|
76
|
+
else:
|
|
77
|
+
for c in checkouts:
|
|
78
|
+
name = c.release.name if c.release else "?"
|
|
79
|
+
print(f"{c.feature or '?'}\t{c.user}\t{c.host}\tv{c.version}\t{name}")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
sys.exit(main())
|
matlab_versions/py.typed
ADDED
|
File without changes
|