maybempi 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max Lindqvist
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: maybempi
3
+ Version: 0.1.0
4
+ Summary: Use MPI only when launched under MPI, and a serial stand-in for mpi4py otherwise.
5
+ Author: Max
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Max Lindqvist
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Changelog, https://github.com/max-models/maybempi/blob/main/CHANGELOG.md
29
+ Project-URL: Documentation, https://max-models.github.io/maybempi/
30
+ Project-URL: Source, https://github.com/max-models/maybempi
31
+ Keywords: hpc,mpi,mpi4py,parallel,serial,slurm
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3 :: Only
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Programming Language :: Python :: 3.14
42
+ Classifier: Topic :: Scientific/Engineering
43
+ Classifier: Topic :: System :: Distributed Computing
44
+ Classifier: Typing :: Typed
45
+ Requires-Python: >=3.10
46
+ Description-Content-Type: text/markdown
47
+ License-File: LICENSE.txt
48
+ Provides-Extra: dev
49
+ Requires-Dist: jupyterlab; extra == "dev"
50
+ Requires-Dist: maybempi[docs,test]; extra == "dev"
51
+ Requires-Dist: pre-commit; extra == "dev"
52
+ Requires-Dist: pyproject-fmt; extra == "dev"
53
+ Requires-Dist: pyright; extra == "dev"
54
+ Requires-Dist: ruff; extra == "dev"
55
+ Provides-Extra: docs
56
+ Requires-Dist: griffe; extra == "docs"
57
+ Requires-Dist: ipykernel; extra == "docs"
58
+ Requires-Dist: nbclient>=0.10; extra == "docs"
59
+ Requires-Dist: nbformat>=5.10; extra == "docs"
60
+ Requires-Dist: numpy; extra == "docs"
61
+ Provides-Extra: mpi
62
+ Requires-Dist: mpi4py; extra == "mpi"
63
+ Provides-Extra: test
64
+ Requires-Dist: numpy; extra == "test"
65
+ Requires-Dist: pytest; extra == "test"
66
+ Requires-Dist: pytest-cov; extra == "test"
67
+ Dynamic: license-file
68
+
69
+ # maybempi
70
+
71
+
72
+ <!-- README.md is generated from README.qmd: edit the .qmd and run `make readme`. -->
73
+
74
+ [![Tests](https://github.com/max-models/maybempi/actions/workflows/test_pytest.yml/badge.svg)](https://github.com/max-models/maybempi/actions/workflows/test_pytest.yml)
75
+ [![Static
76
+ analysis](https://github.com/max-models/maybempi/actions/workflows/static_analysis.yml/badge.svg)](https://github.com/max-models/maybempi/actions/workflows/static_analysis.yml)
77
+ [![Docs](https://github.com/max-models/maybempi/actions/workflows/docs.yml/badge.svg)](https://max-models.github.io/maybempi/)
78
+ [![codecov](https://codecov.io/gh/max-models/maybempi/branch/main/graph/badge.svg)](https://codecov.io/gh/max-models/maybempi)
79
+ [![PyPI](https://img.shields.io/pypi/v/maybempi.png)](https://pypi.org/project/maybempi/)
80
+ [![Python](https://img.shields.io/pypi/pyversions/maybempi.png)](https://pypi.org/project/maybempi/)
81
+
82
+ Use MPI only when the process was launched under MPI, and a serial
83
+ stand-in for `mpi4py` otherwise. The same code runs under
84
+ `mpirun`/`srun` and as a plain `python script.py`, and the serial run
85
+ never starts MPI.
86
+
87
+ Documentation: <https://max-models.github.io/maybempi/>
88
+
89
+ ``` python
90
+ from maybempi import MPI # instead of: from mpi4py import MPI
91
+
92
+ # mpi4py.MPI under mpirun/mpiexec/srun, a serial stand-in otherwise
93
+ comm = MPI.COMM_WORLD
94
+ total = comm.allreduce(local_total, op=MPI.SUM) # local_total in a serial run
95
+ ```
96
+
97
+ ## Why
98
+
99
+ Importing `mpi4py.MPI` calls `MPI_Init`, which costs close to a second
100
+ and makes every collective cost something, even on one process. maybempi
101
+ decides from the environment variables that MPI launchers export (Open
102
+ MPI, MPICH, Intel MPI, PMIx/`srun`, MVAPICH2, Cray), without importing
103
+ mpi4py. In a serial run it returns a stand-in with the module constants
104
+ and communicator methods of mpi4py, which give the results MPI gives on
105
+ one process: `allreduce(x)` is `x`, `gather(x)` is `[x]`,
106
+ `Allreduce(send, recv)` copies. Methods it does not implement raise
107
+ `AttributeError` instead of silently doing nothing.
108
+
109
+ - `MAYBEMPI=0`/`1` overrides the decision.
110
+ - `maybempi.is_serial(MPI)` tells the stand-in from mpi4py.
111
+ - `maybempi.local_rank()` gives the node-local rank before `MPI_Init`,
112
+ e.g. to pick a GPU.
113
+ - The `maybempi` command prints what it decides, and why:
114
+ `mpirun -n 2 maybempi --init`.
115
+
116
+ maybempi is pure Python and has no dependencies.
117
+
118
+ ## Install
119
+
120
+ ``` bash
121
+ pip install maybempi # serial runs only
122
+ pip install "maybempi[mpi]" # with mpi4py, for MPI runs
123
+ ```
124
+
125
+ ## Development
126
+
127
+ With [uv](https://docs.astral.sh/uv/):
128
+
129
+ ``` bash
130
+ make install # uv sync --extra dev, plus the pre-commit hooks
131
+ make lint # ruff check, ruff format --check, pyright
132
+ make test # pytest with coverage
133
+ mpiexec -n 2 .venv/bin/python -m pytest src/maybempi/tests/mpi # tests under a real launcher
134
+ ```
135
+
136
+ Commit messages follow [Conventional
137
+ Commits](https://www.conventionalcommits.org/); see
138
+ [CONTRIBUTING.md](CONTRIBUTING.md).
139
+
140
+ The documentation in `docs/` is an [Astro](https://astro.build/) +
141
+ [Starlight](https://starlight.astro.build/) site with the executed
142
+ notebooks from `tutorials/` and an API reference generated from the
143
+ docstrings. It needs Node 22 or newer:
144
+
145
+ ``` bash
146
+ make docs-install # npm packages and the Python docs extra
147
+ make docs-notebooks # execute tutorials/*.ipynb and convert them to pages
148
+ make docs-dev # live preview at http://localhost:4321/maybempi/
149
+ ```
150
+
151
+ `README.md` is rendered from `README.qmd` with
152
+ [Quarto](https://quarto.org/): `make readme`.
153
+
154
+ ## Releases
155
+
156
+ Before merging a release to `main`, update the version in
157
+ `pyproject.toml`, `src/maybempi/__init__.py` and `CITATION.cff`
158
+ (including its release date), and add the release notes to
159
+ `CHANGELOG.md`. The push to `main` creates a GitHub release with a
160
+ `vX.Y.Z` tag and publishes the package to PyPI with trusted publishing
161
+ (OIDC). See the [publishing
162
+ guide](https://max-models.github.io/maybempi/development/publishing/).
@@ -0,0 +1,94 @@
1
+ # maybempi
2
+
3
+
4
+ <!-- README.md is generated from README.qmd: edit the .qmd and run `make readme`. -->
5
+
6
+ [![Tests](https://github.com/max-models/maybempi/actions/workflows/test_pytest.yml/badge.svg)](https://github.com/max-models/maybempi/actions/workflows/test_pytest.yml)
7
+ [![Static
8
+ analysis](https://github.com/max-models/maybempi/actions/workflows/static_analysis.yml/badge.svg)](https://github.com/max-models/maybempi/actions/workflows/static_analysis.yml)
9
+ [![Docs](https://github.com/max-models/maybempi/actions/workflows/docs.yml/badge.svg)](https://max-models.github.io/maybempi/)
10
+ [![codecov](https://codecov.io/gh/max-models/maybempi/branch/main/graph/badge.svg)](https://codecov.io/gh/max-models/maybempi)
11
+ [![PyPI](https://img.shields.io/pypi/v/maybempi.png)](https://pypi.org/project/maybempi/)
12
+ [![Python](https://img.shields.io/pypi/pyversions/maybempi.png)](https://pypi.org/project/maybempi/)
13
+
14
+ Use MPI only when the process was launched under MPI, and a serial
15
+ stand-in for `mpi4py` otherwise. The same code runs under
16
+ `mpirun`/`srun` and as a plain `python script.py`, and the serial run
17
+ never starts MPI.
18
+
19
+ Documentation: <https://max-models.github.io/maybempi/>
20
+
21
+ ``` python
22
+ from maybempi import MPI # instead of: from mpi4py import MPI
23
+
24
+ # mpi4py.MPI under mpirun/mpiexec/srun, a serial stand-in otherwise
25
+ comm = MPI.COMM_WORLD
26
+ total = comm.allreduce(local_total, op=MPI.SUM) # local_total in a serial run
27
+ ```
28
+
29
+ ## Why
30
+
31
+ Importing `mpi4py.MPI` calls `MPI_Init`, which costs close to a second
32
+ and makes every collective cost something, even on one process. maybempi
33
+ decides from the environment variables that MPI launchers export (Open
34
+ MPI, MPICH, Intel MPI, PMIx/`srun`, MVAPICH2, Cray), without importing
35
+ mpi4py. In a serial run it returns a stand-in with the module constants
36
+ and communicator methods of mpi4py, which give the results MPI gives on
37
+ one process: `allreduce(x)` is `x`, `gather(x)` is `[x]`,
38
+ `Allreduce(send, recv)` copies. Methods it does not implement raise
39
+ `AttributeError` instead of silently doing nothing.
40
+
41
+ - `MAYBEMPI=0`/`1` overrides the decision.
42
+ - `maybempi.is_serial(MPI)` tells the stand-in from mpi4py.
43
+ - `maybempi.local_rank()` gives the node-local rank before `MPI_Init`,
44
+ e.g. to pick a GPU.
45
+ - The `maybempi` command prints what it decides, and why:
46
+ `mpirun -n 2 maybempi --init`.
47
+
48
+ maybempi is pure Python and has no dependencies.
49
+
50
+ ## Install
51
+
52
+ ``` bash
53
+ pip install maybempi # serial runs only
54
+ pip install "maybempi[mpi]" # with mpi4py, for MPI runs
55
+ ```
56
+
57
+ ## Development
58
+
59
+ With [uv](https://docs.astral.sh/uv/):
60
+
61
+ ``` bash
62
+ make install # uv sync --extra dev, plus the pre-commit hooks
63
+ make lint # ruff check, ruff format --check, pyright
64
+ make test # pytest with coverage
65
+ mpiexec -n 2 .venv/bin/python -m pytest src/maybempi/tests/mpi # tests under a real launcher
66
+ ```
67
+
68
+ Commit messages follow [Conventional
69
+ Commits](https://www.conventionalcommits.org/); see
70
+ [CONTRIBUTING.md](CONTRIBUTING.md).
71
+
72
+ The documentation in `docs/` is an [Astro](https://astro.build/) +
73
+ [Starlight](https://starlight.astro.build/) site with the executed
74
+ notebooks from `tutorials/` and an API reference generated from the
75
+ docstrings. It needs Node 22 or newer:
76
+
77
+ ``` bash
78
+ make docs-install # npm packages and the Python docs extra
79
+ make docs-notebooks # execute tutorials/*.ipynb and convert them to pages
80
+ make docs-dev # live preview at http://localhost:4321/maybempi/
81
+ ```
82
+
83
+ `README.md` is rendered from `README.qmd` with
84
+ [Quarto](https://quarto.org/): `make readme`.
85
+
86
+ ## Releases
87
+
88
+ Before merging a release to `main`, update the version in
89
+ `pyproject.toml`, `src/maybempi/__init__.py` and `CITATION.cff`
90
+ (including its release date), and add the release notes to
91
+ `CHANGELOG.md`. The push to `main` creates a GitHub release with a
92
+ `vX.Y.Z` tag and publishes the package to PyPI with trusted publishing
93
+ (OIDC). See the [publishing
94
+ guide](https://max-models.github.io/maybempi/development/publishing/).
@@ -0,0 +1,82 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = [ "setuptools", "wheel" ]
4
+
5
+ [project]
6
+ name = "maybempi"
7
+ version = "0.1.0"
8
+ description = "Use MPI only when launched under MPI, and a serial stand-in for mpi4py otherwise."
9
+ readme = "README.md"
10
+ keywords = [ "hpc", "mpi", "mpi4py", "parallel", "serial", "slurm" ]
11
+ license = { file = "LICENSE.txt" }
12
+ authors = [ { name = "Max" } ]
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ "Topic :: Scientific/Engineering",
26
+ "Topic :: System :: Distributed Computing",
27
+ "Typing :: Typed",
28
+ ]
29
+ dependencies = []
30
+ optional-dependencies.dev = [
31
+ "jupyterlab",
32
+ "maybempi[docs,test]",
33
+ "pre-commit",
34
+ "pyproject-fmt",
35
+ "pyright",
36
+ "ruff",
37
+ ]
38
+ optional-dependencies.docs = [
39
+ "griffe",
40
+ "ipykernel",
41
+ "nbclient>=0.10",
42
+ "nbformat>=5.10",
43
+ "numpy",
44
+ ]
45
+ optional-dependencies.mpi = [ "mpi4py" ]
46
+ optional-dependencies.test = [ "numpy", "pytest", "pytest-cov" ]
47
+ urls.Changelog = "https://github.com/max-models/maybempi/blob/main/CHANGELOG.md"
48
+ urls.Documentation = "https://max-models.github.io/maybempi/"
49
+ urls.Source = "https://github.com/max-models/maybempi"
50
+ scripts.maybempi = "maybempi.cli:main"
51
+
52
+ [tool.setuptools]
53
+ package-data."*" = [ "py.typed" ]
54
+ packages.find.where = [ "src" ]
55
+
56
+ [tool.ruff]
57
+ target-version = "py310"
58
+ line-length = 88
59
+ extend-include = [ "*.ipynb" ]
60
+ # pycodestyle, pyflakes, isort, pyupgrade, bugbear and pydocstyle
61
+ lint.select = [ "B", "D", "E", "F", "I", "UP", "W" ]
62
+ lint.ignore = [ "E501" ] # line length is handled by the formatter
63
+ lint.per-file-ignores."**/tests/**" = [ "D" ]
64
+ lint.per-file-ignores."*.ipynb" = [ "D" ]
65
+ lint.per-file-ignores."docs/tools/*" = [ "D" ]
66
+ lint.pydocstyle.convention = "google"
67
+
68
+ [tool.pyright]
69
+ include = [ "src" ]
70
+ exclude = [ "**/tests" ]
71
+ pythonVersion = "3.10"
72
+ typeCheckingMode = "standard"
73
+
74
+ [tool.pytest]
75
+ ini_options.pythonpath = [ "src" ]
76
+ ini_options.testpaths = [ "src" ]
77
+ ini_options.addopts = "-ra"
78
+
79
+ [tool.coverage]
80
+ run.omit = [ "*/tests/*" ]
81
+ run.source = [ "maybempi" ]
82
+ report.show_missing = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,66 @@
1
+ """Use MPI only when the process was launched under MPI, and a serial stand-in otherwise.
2
+
3
+ Importing ``mpi4py.MPI`` starts MPI, which costs close to a second and makes every
4
+ collective cost something, even on one process. maybempi decides from the
5
+ environment the launcher sets up, without importing mpi4py, and returns either
6
+ ``mpi4py.MPI`` or a serial stand-in with the same interface::
7
+
8
+ import maybempi
9
+
10
+ MPI = maybempi.get_mpi() # mpi4py.MPI under mpirun/srun, else the stand-in
11
+ comm = MPI.COMM_WORLD
12
+ total = comm.allreduce(local_total, op=MPI.SUM)
13
+
14
+ ``from maybempi import MPI`` is the drop-in replacement for ``from mpi4py import
15
+ MPI``: the name resolves to ``get_mpi()`` when it is first imported.
16
+
17
+ :mod:`maybempi.launch` holds the detection, :mod:`maybempi.serial` the stand-in.
18
+ """
19
+
20
+ from typing import Any
21
+
22
+ from maybempi.launch import (
23
+ LAUNCHER_VARIABLES,
24
+ LOCAL_RANK_VARIABLES,
25
+ OVERRIDE_VARIABLE,
26
+ get_mpi,
27
+ is_serial,
28
+ launched_under_mpi,
29
+ launcher_variable,
30
+ local_rank,
31
+ )
32
+ from maybempi.serial import (
33
+ SerialComm,
34
+ SerialMPI,
35
+ SerialPrequest,
36
+ SerialRequest,
37
+ SerialStatus,
38
+ set_copy_hook,
39
+ )
40
+
41
+ __version__ = "0.1.0"
42
+
43
+ __all__ = [
44
+ "LAUNCHER_VARIABLES",
45
+ "LOCAL_RANK_VARIABLES",
46
+ "OVERRIDE_VARIABLE",
47
+ "SerialComm",
48
+ "SerialMPI",
49
+ "SerialPrequest",
50
+ "SerialRequest",
51
+ "SerialStatus",
52
+ "__version__",
53
+ "get_mpi",
54
+ "is_serial",
55
+ "launched_under_mpi",
56
+ "launcher_variable",
57
+ "local_rank",
58
+ "set_copy_hook",
59
+ ]
60
+
61
+
62
+ def __getattr__(name: str) -> Any:
63
+ """Resolve ``maybempi.MPI`` (and ``from maybempi import MPI``) to :func:`get_mpi`."""
64
+ if name == "MPI":
65
+ return get_mpi()
66
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,5 @@
1
+ """``python -m maybempi``: the same as the ``maybempi`` command."""
2
+
3
+ from maybempi.cli import main
4
+
5
+ main()
@@ -0,0 +1,142 @@
1
+ """The ``maybempi`` command: show what maybempi decides in this environment.
2
+
3
+ Run it the way the application is run, to see whether it would use MPI::
4
+
5
+ maybempi # serial: one table
6
+ mpirun -n 2 maybempi # one table per rank (MPI is not started)
7
+ mpirun -n 2 maybempi --init # start MPI: rank 0 prints one table for all ranks
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import importlib.util
14
+ import os
15
+ import socket
16
+ from collections.abc import Sequence
17
+ from typing import Any
18
+
19
+ from maybempi import __version__
20
+ from maybempi.launch import (
21
+ OVERRIDE_VARIABLE,
22
+ get_mpi,
23
+ is_serial,
24
+ launched_under_mpi,
25
+ launcher_variable,
26
+ local_rank,
27
+ )
28
+
29
+ # columns of the per-rank table, in this order; other items become columns only
30
+ # if they differ between ranks
31
+ _RANK_COLUMNS = ("rank", "host", "local rank", "launcher variable")
32
+
33
+
34
+ def info(init: bool = False) -> dict[str, Any]:
35
+ """Collect what maybempi decides in this process, and why.
36
+
37
+ Args:
38
+ init: Also call :func:`~maybempi.launch.get_mpi` (which starts MPI under a
39
+ launcher) and add the rank and size of ``COMM_WORLD``.
40
+
41
+ Returns:
42
+ The items of the report, by name.
43
+ """
44
+ items: dict[str, Any] = {
45
+ "maybempi": __version__,
46
+ "host": socket.gethostname(),
47
+ "launched under MPI": launched_under_mpi(),
48
+ "launcher variable": launcher_variable() or "-",
49
+ f"{OVERRIDE_VARIABLE} override": os.environ.get(OVERRIDE_VARIABLE, "-"),
50
+ "local rank": local_rank(),
51
+ "mpi4py installed": importlib.util.find_spec("mpi4py") is not None,
52
+ }
53
+ if init:
54
+ mpi = get_mpi()
55
+ comm = mpi.COMM_WORLD
56
+ items["MPI"] = "serial stand-in" if is_serial(mpi) else "mpi4py"
57
+ items["rank"] = comm.Get_rank()
58
+ items["size"] = comm.Get_size()
59
+ return items
60
+
61
+
62
+ def _table(header: Sequence[str], rows: Sequence[Sequence[Any]]) -> str:
63
+ """Format `rows` as a plain-text table with aligned columns."""
64
+ cells = [[str(value) for value in row] for row in rows]
65
+ widths = [
66
+ max(len(str(h)), *(len(row[i]) for row in cells)) for i, h in enumerate(header)
67
+ ]
68
+
69
+ def line(values: Sequence[str]) -> str:
70
+ return " ".join(
71
+ v.ljust(w) for v, w in zip(values, widths, strict=True)
72
+ ).rstrip()
73
+
74
+ return "\n".join(
75
+ [line(header), line(["-" * w for w in widths]), *(line(row) for row in cells)]
76
+ )
77
+
78
+
79
+ def format_report(items: dict[str, Any]) -> str:
80
+ """Format the report of one process as a two-column table.
81
+
82
+ Args:
83
+ items: What :func:`info` returned.
84
+
85
+ Returns:
86
+ The table, one item per line.
87
+ """
88
+ items = dict(items)
89
+ if "rank" in items:
90
+ items["rank"] = f"{items['rank']} of {items.pop('size')}"
91
+ return _table(("item", "value"), list(items.items()))
92
+
93
+
94
+ def format_reports(reports: Sequence[dict[str, Any]]) -> str:
95
+ """Format the reports of all ranks: the common items, then one row per rank.
96
+
97
+ Args:
98
+ reports: What :func:`info` returned on each rank, in rank order.
99
+
100
+ Returns:
101
+ A table of the items that are the same on every rank, and a table with one
102
+ row per rank for the others (rank, host, local rank, launcher variable and
103
+ whatever else differs).
104
+ """
105
+ names = list(reports[0])
106
+ differs = {n for n in names if any(r.get(n) != reports[0].get(n) for r in reports)}
107
+ columns = [n for n in _RANK_COLUMNS if n in names]
108
+ columns += [n for n in names if n in differs and n not in columns]
109
+ common = [(n, reports[0][n]) for n in names if n not in columns]
110
+ rows = [[report.get(n, "-") for n in columns] for report in reports]
111
+ return f"{_table(('item', 'value'), common)}\n\n{_table(columns, rows)}"
112
+
113
+
114
+ def report(init: bool = False) -> str:
115
+ """Return the report of this process as a table (see :func:`info`)."""
116
+ return format_report(info(init))
117
+
118
+
119
+ def main(argv: list[str] | None = None) -> None:
120
+ """Print the report; the entry point of the ``maybempi`` command."""
121
+ parser = argparse.ArgumentParser(
122
+ prog="maybempi",
123
+ description="Show whether this process would use MPI, and why.",
124
+ )
125
+ parser.add_argument(
126
+ "--init",
127
+ action="store_true",
128
+ help="also start MPI (under a launcher); rank 0 then prints one table for all ranks",
129
+ )
130
+ parser.add_argument("--version", action="version", version=__version__)
131
+ args = parser.parse_args(argv)
132
+ items = info(init=args.init)
133
+ if args.init and items["size"] > 1:
134
+ reports = get_mpi().COMM_WORLD.gather(items, root=0)
135
+ if items["rank"] == 0:
136
+ print(format_reports(reports), flush=True)
137
+ return
138
+ print(format_report(items), flush=True)
139
+
140
+
141
+ if __name__ == "__main__":
142
+ main()