Skip to content

Commit a8e933a

Browse files
authored
Merge pull request #86 from finsberg/cli
Add skeleton for cli
2 parents 75eb88a + a961e2a commit a8e933a

4 files changed

Lines changed: 200 additions & 42 deletions

File tree

.cspell_dict.txt

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,115 @@
1+
Alfio
12
allclose
23
allreduce
34
argsort
45
astype
56
atol
7+
Bartolucci
68
basix
79
bcast
10+
bestel
11+
Bestel
12+
booktitle
13+
Britton
814
Bursi
15+
caplog
16+
cardiomyocytes
917
cellname
18+
Chiara
1019
cofac
20+
cpus
21+
Cristobal
1122
Culloch
1223
deviatoric
1324
dirichletbc
1425
dofmap
1526
dofs
1627
dolfinx
1728
dvlp
29+
Elife
30+
Elsevier
1831
endo
1932
Erlicher
33+
Fedele
2034
fenics
2135
fenicsx
2236
fenicsx_pulse
2337
ffun
2438
finsberg
39+
Francesco
2540
functionspace
2641
geodir
2742
gradu
2843
Guccione
44+
Holohan
2945
Holzapfel
3046
holzapfelogden
3147
Hookean
3248
hstack
3349
hyperelastic
50+
inproceedings
3451
Intracomm
3552
isclose
3653
isochoric
3754
isscalar
55+
Jakub
56+
Kluwer
57+
Laszlo
58+
levelname
3859
libgl
3960
libxrender
4061
linalg
4162
lmbda
4263
Luca
64+
Matteo
4365
mechanicsproblem
66+
ment
4467
meshtags
68+
MICCAI
69+
Minchole
4570
Mises
4671
multiplicatively
72+
myocyte
4773
nabla
4874
ndarray
4975
neohookean
5076
Neumann
77+
Niederer
5178
numpy
5279
Oreste
80+
Orovio
5381
outdir
5482
Paraview
83+
Passini
5584
pendo
5685
petsc
5786
Piola
5887
preonly
5988
PYVISTA
89+
Quarteroni
90+
regazzoni
91+
Regazzoni
92+
Remedios
93+
rique
6094
rtol
6195
scifem
96+
Severi
97+
Shrier
6298
Silvano
99+
Sorine
100+
Stefano
63101
subplus
102+
tomek
103+
Tomek
64104
TRAME
65105
ureg
66106
varepsilon
67107
Venant
68108
Verlag
109+
Virag
69110
viscoelastic
70111
viscoelasticity
71112
Waldman
72113
XDMF
73114
xvfb
74-
75-
76-
inproceedings
77-
bestel
78-
Bestel
79-
ment
80-
rique
81-
Sorine
82-
booktitle
83-
MICCAI
84-
Kluwer
85-
regazzoni
86-
Regazzoni
87-
Francesco
88-
Matteo
89-
Fedele
90-
Quarteroni
91-
Alfio
92-
Elsevier
93-
tomek
94-
myocyte
95-
Tomek
96-
Jakub
97-
Orovio
98-
Passini
99115
Zhou
100-
Minchole
101-
Britton
102-
Bartolucci
103-
Chiara
104-
Severi
105-
Stefano
106-
Shrier
107-
Virag
108-
Laszlo
109-
Elife
110-
cardiomyocytes
111-
Holohan
112-
Remedios
113-
Cristobal
114-
Niederer
115-
Elsevier

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ authors = [{name = "Henrik Finsberg", email = "henriknf@simula.no"}]
99
license = {file = "LICENSE"}
1010
readme = "README.md"
1111
requires-python = ">=3.10.0"
12-
dependencies = ["fenics-dolfinx", "pint", "scifem"]
12+
dependencies = ["fenics-dolfinx", "pint", "scifem", "rich", "rich-argparse"]
13+
14+
[project.scripts]
15+
pulse = "fenicsx_pulse.cli:main"
1316

1417
[project.optional-dependencies]
1518
test = [

src/fenicsx_pulse/cli.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import argparse
2+
import logging
3+
from typing import Optional, Sequence
4+
5+
from rich_argparse import ArgumentDefaultsRichHelpFormatter
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def setup_parser():
11+
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsRichHelpFormatter)
12+
# Root parser
13+
parser.add_argument(
14+
"--dry-run",
15+
action="store_true",
16+
help="Just print the command and do not run it",
17+
)
18+
parser.add_argument(
19+
"-v",
20+
"--verbose",
21+
action="store_true",
22+
help="Print more information",
23+
)
24+
parser.add_argument(
25+
"--log-all-cpus",
26+
action="store_true",
27+
help="Log on all CPUs",
28+
)
29+
30+
subparsers = parser.add_subparsers(dest="command")
31+
32+
# Version parser
33+
subparsers.add_parser("version", help="Display version information")
34+
35+
# Run simulation parser
36+
subparsers.add_parser("run", help="Run simulations")
37+
38+
# Validate configuration parser
39+
subparsers.add_parser("validate-config", help="Validate the configuration file")
40+
41+
# Postprocessing parser
42+
subparsers.add_parser("post", help="Postprocessing")
43+
44+
return parser
45+
46+
47+
def setup_logging(level: int = logging.INFO, log_all_cpus: bool = False):
48+
from mpi4py import MPI
49+
50+
from rich.console import Console
51+
from rich.logging import RichHandler
52+
from rich.theme import Theme
53+
54+
comm = MPI.COMM_WORLD
55+
rank = comm.rank
56+
size = comm.size
57+
58+
FORMAT = "%(asctime)s %(rank)s%(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
59+
60+
class Formatter(logging.Formatter):
61+
def format(self, record):
62+
record.rank = f"CPU {rank}: " if size > 1 else ""
63+
return super().format(record)
64+
65+
class MPIFilter(logging.Filter):
66+
def filter(self, record):
67+
if rank == 0:
68+
return 1
69+
else:
70+
return 0
71+
72+
console = Console(theme=Theme({"logging.level.custom": "green"}), width=140)
73+
handler = RichHandler(level=level, console=console)
74+
75+
handler.setFormatter(Formatter(FORMAT))
76+
if not log_all_cpus:
77+
handler.addFilter(MPIFilter())
78+
79+
logging.basicConfig(
80+
level="NOTSET",
81+
format=FORMAT,
82+
handlers=[handler],
83+
)
84+
85+
_disable_loggers()
86+
87+
88+
def _disable_loggers():
89+
for name in ["matplotlib"]:
90+
logging.getLogger(name).setLevel(logging.WARNING)
91+
92+
93+
def display_version_info():
94+
from mpi4py import MPI
95+
from petsc4py import PETSc
96+
97+
import dolfinx
98+
99+
from . import __version__
100+
101+
logger.info(f"fenicsx-pulse: {__version__}")
102+
logger.info(f"dolfinx: {dolfinx.__version__}")
103+
logger.info(f"mpi4py: {MPI.Get_version()}")
104+
logger.info(f"petsc4py: {PETSc.Sys.getVersion()}")
105+
106+
107+
def dispatch(parser: argparse.ArgumentParser, argv: Optional[Sequence[str]] = None) -> int:
108+
args = vars(parser.parse_args(argv))
109+
level = logging.DEBUG if args.pop("verbose") else logging.INFO
110+
log_all_cpus = args.pop("log_all_cpus")
111+
setup_logging(level=level, log_all_cpus=log_all_cpus)
112+
113+
dry_run = args.pop("dry_run")
114+
command = args.pop("command")
115+
116+
if dry_run:
117+
logger.info("Dry run: %s", command)
118+
logger.info("Arguments: %s", args)
119+
return 0
120+
121+
try:
122+
if command == "version":
123+
display_version_info()
124+
elif command == "run":
125+
return NotImplemented
126+
127+
elif command == "validate-config":
128+
return NotImplemented
129+
elif command == "post":
130+
return NotImplemented
131+
else:
132+
logger.error(f"Unknown command {command}")
133+
parser.print_help()
134+
return 1
135+
except ValueError as e:
136+
logger.error(e)
137+
return 1
138+
139+
return 0
140+
141+
142+
def main(argv: Optional[Sequence[str]] = None) -> int:
143+
parser = setup_parser()
144+
return dispatch(parser, argv)

tests/test_cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import logging
2+
3+
import fenicsx_pulse
4+
import fenicsx_pulse.cli
5+
6+
7+
def test_version(caplog):
8+
caplog.set_level(logging.INFO)
9+
ret = fenicsx_pulse.cli.main(["version"])
10+
assert ret == 0
11+
assert caplog.records[0].msg == f"fenicsx-pulse: {fenicsx_pulse.__version__}"

0 commit comments

Comments
 (0)