Skip to content

Commit f4bb14b

Browse files
jakehaderopotowsky
andauthored
Update the EntryPoint class to provide user feedback on required positional arguments (#922)
* Update the entry point class to provide the settings file as an required position argument when a user requests the help menu. Additionally, add a try/except around adding a new argument from case settings to handle duplicate entries more robustly rather than requiring exclusion of settings like `verbosity` and `branchVerbosity`. * Suppress the printing of all possible settings in clone CLI * There are 18 entry points! * Make changes to checkInputs CLI and tests Having both a settings file and patterns arg is both redundant and causes unreliable use issues. Since only patterns was used in invoke, decided to remove the settingsArgument as optional. I also removed the sys.exit(), since there is no need that I can think of to do this. The armi unit test settings file, e.g., fails this check (with can start = UNKNOWN). * Move settings arg processing to not be first (nargs='*' behavior means this can't be first in the list) * Minor docstring add * Remove reloadDBName setting from armiRun.yaml This is only needed for 3 tests, and it was causing some annoying failures on the check-inputs entry point test (although I fixed it separately by rejecting the notion that there needed to be a sys.exit()). * Update warning to error for checkInputs CLI * Remove unused CLI args from checkInputs (finishing what #754 started) * Remove update to real file because that ain't necessary * Release Notes Co-authored-by: Arrielle Opotowsky <[email protected]>
1 parent 3d6a992 commit f4bb14b

File tree

10 files changed

+130
-97
lines changed

10 files changed

+130
-97
lines changed

armi/bookkeeping/db/tests/test_comparedb3.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ def test_diffResultsBasic(self):
8686
def test_compareDatabaseDuplicate(self):
8787
"""end-to-end test of compareDatabases() on a photocopy database"""
8888
# build two super-simple H5 files for testing
89-
o, r = test_reactors.loadTestReactor(TEST_ROOT)
89+
o, r = test_reactors.loadTestReactor(
90+
TEST_ROOT, customSettings={"reloadDBName": "reloadingDB.h5"}
91+
)
9092

9193
# create two DBs, identical but for file names
9294
dbs = []
@@ -115,7 +117,9 @@ def test_compareDatabaseDuplicate(self):
115117
def test_compareDatabaseSim(self):
116118
"""end-to-end test of compareDatabases() on very simlar databases"""
117119
# build two super-simple H5 files for testing
118-
o, r = test_reactors.loadTestReactor(TEST_ROOT)
120+
o, r = test_reactors.loadTestReactor(
121+
TEST_ROOT, customSettings={"reloadDBName": "reloadingDB.h5"}
122+
)
119123

120124
# create two DBs, identical but for file names
121125
dbs = []

armi/bookkeeping/db/tests/test_database3.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ class TestDatabase3(unittest.TestCase):
3838
def setUp(self):
3939
self.td = TemporaryDirectoryChanger()
4040
self.td.__enter__()
41-
self.o, self.r = test_reactors.loadTestReactor(TEST_ROOT)
41+
self.o, self.r = test_reactors.loadTestReactor(
42+
TEST_ROOT, customSettings={"reloadDBName": "reloadingDB.h5"}
43+
)
4244

4345
self.dbi = database3.DatabaseInterface(self.r, self.o.cs)
4446
self.dbi.initDB(fName=self._testMethodName + ".h5")
@@ -180,8 +182,11 @@ def test_prepRestartRun(self):
180182
created here for this test.
181183
"""
182184
# first successfully call to prepRestartRun
183-
o, r = test_reactors.loadTestReactor(TEST_ROOT)
185+
o, r = test_reactors.loadTestReactor(
186+
TEST_ROOT, customSettings={"reloadDBName": "reloadingDB.h5"}
187+
)
184188
cs = o.cs
189+
185190
ratedPower = cs["power"]
186191
startCycle = cs["startCycle"]
187192
startNode = cs["startNode"]

armi/cli/checkInputs.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ class CheckInputEntryPoint(EntryPoint):
6060
"""
6161

6262
name = "check-input"
63-
settingsArgument = "optional"
6463

6564
def addOptions(self):
6665
self.parser.add_argument(
@@ -70,19 +69,6 @@ def addOptions(self):
7069
default=False,
7170
help="Generate a report to summarize the inputs",
7271
)
73-
self.parser.add_argument(
74-
"--full-core-map",
75-
"-m",
76-
action="store_true",
77-
default=False,
78-
help="Generate the full core reactor map in the design report",
79-
)
80-
self.parser.add_argument(
81-
"--disable-block-axial-mesh",
82-
action="store_true",
83-
default=False,
84-
help="Remove the additional block axial mesh points on the assembly type figure(s)",
85-
)
8672
self.parser.add_argument(
8773
"--recursive",
8874
"-r",
@@ -118,16 +104,15 @@ def invoke(self):
118104
if not self.args.skip_checks:
119105
hasIssues = "PASSED" if case.checkInputs() else "HAS ISSUES"
120106

121-
try:
122-
if self.args.generate_design_summary:
107+
canStart = "UNKNOWN"
108+
if self.args.generate_design_summary:
109+
try:
123110
case.summarizeDesign()
124111
canStart = "PASSED"
125-
else:
126-
canStart = "UNKNOWN"
127-
except Exception:
128-
runLog.error("Failed to initialize/summarize {}".format(case))
129-
runLog.error(traceback.format_exc())
130-
canStart = "FAILED"
112+
except Exception:
113+
runLog.error("Failed to initialize/summarize {}".format(case))
114+
runLog.error(traceback.format_exc())
115+
canStart = "FAILED"
131116

132117
table.append((case.cs.path, case.title, canStart, hasIssues))
133118

@@ -139,5 +124,8 @@ def invoke(self):
139124
)
140125
)
141126

142-
if any(t[2] != "PASSED" or t[3] != "PASSED" for t in table):
143-
sys.exit(-1)
127+
if any(t[3] == "HAS ISSUES" for t in table):
128+
runLog.error("The case is not self consistent")
129+
130+
if any(t[2] == "FAILED" for t in table):
131+
runLog.error("The case can not start")

armi/cli/clone.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,16 @@ class CloneArmiRunCommandBatch(EntryPoint):
2828
settingsArgument = "required"
2929

3030
def addOptions(self):
31-
for settingName in self.cs.keys():
32-
# verbosity and branchVerbosity already have command line options in the default parser
33-
# adding them again would result in an error from argparse.
34-
if settingName not in ["verbosity", "branchVerbosity"]:
35-
self.createOptionFromSetting(settingName)
3631
self.parser.add_argument(
3732
"--additional-files",
3833
nargs="*",
3934
default=[],
4035
help="Additional files from the source directory to copy into the target directory",
4136
)
37+
# somehow running `armi clone-batch -h` on the command line requires this to
38+
# not be first?
39+
for settingName in self.cs.keys():
40+
self.createOptionFromSetting(settingName, suppressHelp=True)
4241

4342
def invoke(self):
4443
# get the case title.
@@ -65,10 +64,8 @@ class CloneSuiteCommand(EntryPoint):
6564

6665
def addOptions(self):
6766
for settingName in self.cs.environmentSettings:
68-
# verbosity and branchVerbosity already have command line options in the default parser
69-
# adding them again would result in an error from argparse.
70-
if settingName not in {"verbosity", "branchVerbosity"}:
71-
self.createOptionFromSetting(settingName)
67+
self.createOptionFromSetting(settingName)
68+
7269
self.parser.add_argument(
7370
"--directory",
7471
"-d",

armi/cli/entryPoint.py

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,53 @@ class EntryPoint:
9090
def __init__(self):
9191
if self.name is None:
9292
raise AttributeError(
93-
"Subclasses of EntryPoint must define a `name` class attribute"
93+
f"Subclasses of EntryPoint must define a `name` class attribute"
9494
)
9595

96+
self.cs = self._initSettings()
97+
settings.setMasterCs(self.cs)
98+
9699
self.parser = argparse.ArgumentParser(
97100
prog="{} {}".format(context.APP_NAME, self.name),
98101
description=self.description or self.__doc__,
99102
)
103+
if self.settingsArgument is not None:
104+
if self.settingsArgument not in ["required", "optional"]:
105+
raise AttributeError(
106+
f"Subclasses of EntryPoint must specify if the a case settings file is `required` or `optional`"
107+
)
108+
if self.settingsArgument == "optional":
109+
self.parser.add_argument(
110+
"settings_file",
111+
nargs="?",
112+
action=loadSettings(self.cs),
113+
help="path to the settings file to load.",
114+
)
115+
elif self.settingsArgument == "required":
116+
self.parser.add_argument(
117+
"settings_file",
118+
action=loadSettings(self.cs),
119+
help="path to the settings file to load.",
120+
)
100121

101-
self.args = argparse.Namespace()
122+
# optional arguments
123+
self.parser.add_argument(
124+
"--caseTitle",
125+
type=str,
126+
nargs=None,
127+
action=setCaseTitle(self.cs),
128+
help="update the case title of the run.",
129+
)
130+
self.parser.add_argument(
131+
"--batch",
132+
action="store_true",
133+
default=False,
134+
help="Run in batch mode even on TTY, silencing all queries.",
135+
)
136+
self.createOptionFromSetting("verbosity", "-v")
137+
self.createOptionFromSetting("branchVerbosity", "-V")
102138

103-
self.cs = self._initSettings()
104-
settings.setMasterCs(self.cs)
139+
self.args = argparse.Namespace()
105140
self.settingsProvidedOnCommandLine = []
106141

107142
@staticmethod
@@ -129,7 +164,6 @@ def addOptions(self):
129164
130165
argparse.ArgumentParser.add_argument : Often called from here using
131166
``self.parser.add_argument`` to add custom argparse arguments.
132-
133167
"""
134168

135169
def parse_args(self, args):
@@ -140,37 +174,6 @@ def parse(self, args):
140174
"""
141175
Parse the command line arguments, with the command specific arguments.
142176
"""
143-
144-
if self.settingsArgument == "optional":
145-
self.parser.add_argument(
146-
"settings_file",
147-
nargs="?",
148-
action=loadSettings(self.cs),
149-
help="path to the settings file to load.",
150-
)
151-
elif self.settingsArgument == "required":
152-
self.parser.add_argument(
153-
"settings_file",
154-
action=loadSettings(self.cs),
155-
help="path to the settings file to load.",
156-
)
157-
# optional arguments
158-
self.parser.add_argument(
159-
"--caseTitle",
160-
type=str,
161-
nargs=None,
162-
action=setCaseTitle(self.cs),
163-
help="update the case title of the run.",
164-
)
165-
self.parser.add_argument(
166-
"--batch",
167-
action="store_true",
168-
default=False,
169-
help="Run in batch mode even on TTY, silencing all queries.",
170-
)
171-
self.createOptionFromSetting("verbosity", "-v")
172-
self.createOptionFromSetting("branchVerbosity", "-V")
173-
174177
self.addOptions()
175178
self.parse_args(args)
176179

@@ -233,15 +236,21 @@ def createOptionFromSetting(
233236

234237
isListType = settingsInstance.underlyingType == list
235238

236-
self.parser.add_argument(
237-
*aliases,
238-
type=str, # types are properly converted by _SetSettingAction
239-
nargs="*" if isListType else None,
240-
action=setSetting(self),
241-
default=settingsInstance.default,
242-
choices=choices,
243-
help=helpMessage
244-
)
239+
try:
240+
self.parser.add_argument(
241+
*aliases,
242+
type=str, # types are properly converted by _SetSettingAction
243+
nargs="*" if isListType else None,
244+
action=setSetting(self),
245+
default=settingsInstance.default,
246+
choices=choices,
247+
help=helpMessage,
248+
)
249+
# Capture an argument error here to prevent errors when duplicate options are attempting
250+
# to be added. This may also be captured by exploring the parser's `_actions` list as well
251+
# but this avoid accessing a private attribute.
252+
except argparse.ArgumentError:
253+
pass
245254

246255
def _createToggleFromSetting(self, settingName, helpMessage, additionalAlias=None):
247256
aliases = ["--" + settingName]

armi/cli/modify.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,7 @@ def addOptions(self):
7676
help="Pattern(s) to use to find match file names (e.g. *.yaml)",
7777
)
7878
for settingName in self.cs.keys():
79-
# verbosity and branchVerbosity already have command line options in the default parser
80-
# adding them again would result in an error from argparse.
81-
if settingName not in ["verbosity", "branchVerbosity"]:
82-
# can't modify case title, just use clone
83-
self.createOptionFromSetting(settingName, suppressHelp=True)
79+
self.createOptionFromSetting(settingName, suppressHelp=True)
8480

8581
def invoke(self):
8682
csInstances = settings.recursivelyLoadSettingsFiles(

armi/cli/tests/test_runEntryPoint.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import unittest
2222

2323
from armi.__main__ import main
24+
from armi.cli.entryPoint import EntryPoint
2425
from armi.bookkeeping.visualization.entryPoint import VisFileEntryPoint
2526
from armi.cli.checkInputs import CheckInputEntryPoint, ExpandBlueprints
2627
from armi.cli.clone import CloneArmiRunCommandBatch, CloneSuiteCommand
@@ -32,38 +33,71 @@
3233
from armi.cli.run import RunEntryPoint
3334
from armi.cli.runSuite import RunSuiteCommand
3435
from armi.physics.neutronics.diffIsotxs import CompareIsotxsLibraries
35-
from armi.tests import mockRunLogs, TEST_ROOT
36+
from armi.tests import mockRunLogs, TEST_ROOT, ARMI_RUN_PATH
3637
from armi.utils.directoryChangers import TemporaryDirectoryChanger
38+
from armi.utils.dynamicImporter import getEntireFamilyTree
39+
40+
41+
class TestInitializationEntryPoints(unittest.TestCase):
42+
def test_entryPointInitialization(self):
43+
"""Tests the initialization of all subclasses of `EntryPoint`."""
44+
entryPoints = getEntireFamilyTree(EntryPoint)
45+
46+
# Note that this is a hard coded number that should be incremented
47+
# if a new ARMI framework entry point is added. This is a bit hacky,
48+
# but will help demonstrate that entry point classes can be initialized
49+
# and the `addOptions` and public API is tested.
50+
self.assertEqual(len(entryPoints), 18)
51+
52+
for e in entryPoints:
53+
entryPoint = e()
54+
entryPoint.addOptions()
55+
settingsArg = None
56+
if entryPoint.settingsArgument is not None:
57+
for a in entryPoint.parser._actions:
58+
if "settings_file" in a.dest:
59+
settingsArg = a
60+
break
61+
self.assertIsNotNone(
62+
settingsArg,
63+
msg=(
64+
f"A settings file argument was expected for {entryPoint}, "
65+
f"but does not exist. This is a error in the EntryPoint "
66+
f"implementation."
67+
),
68+
)
3769

3870

3971
class TestCheckInputEntryPoint(unittest.TestCase):
4072
def test_checkInputEntryPointBasics(self):
4173
ci = CheckInputEntryPoint()
4274
ci.addOptions()
43-
ci.parse_args(["/path/to/fake.yaml"])
75+
ci.parse_args(["/path/to/fake.yaml", "-C"])
4476

4577
self.assertEqual(ci.name, "check-input")
46-
self.assertEqual(ci.settingsArgument, "optional")
78+
self.assertEqual(ci.args.patterns, ["/path/to/fake.yaml"])
79+
self.assertEqual(ci.args.skip_checks, True)
80+
self.assertEqual(ci.args.generate_design_summary, False)
4781

4882
def test_checkInputEntryPointInvoke(self):
4983
ci = CheckInputEntryPoint()
5084
ci.addOptions()
51-
ci.parse_args(["/path/to/fake.yaml"])
85+
ci.parse_args([ARMI_RUN_PATH])
5286

5387
with mockRunLogs.BufferLog() as mock:
5488
self.assertEqual("", mock._outputStream)
5589

5690
ci.invoke()
5791

58-
self.assertIn("/path/to/fake.yaml", mock._outputStream)
92+
self.assertIn(ARMI_RUN_PATH, mock._outputStream)
5993
self.assertIn("input is self consistent", mock._outputStream)
6094

6195

6296
class TestCloneArmiRunCommandBatch(unittest.TestCase):
6397
def test_cloneArmiRunCommandBatchBasics(self):
6498
ca = CloneArmiRunCommandBatch()
6599
ca.addOptions()
66-
ca.parse_args(["--additional-files", "test"])
100+
ca.parse_args([ARMI_RUN_PATH, "--additional-files", "test"])
67101

68102
self.assertEqual(ca.name, "clone-batch")
69103
self.assertEqual(ca.settingsArgument, "required")
@@ -237,7 +271,7 @@ class TestRunEntryPoint(unittest.TestCase):
237271
def test_runEntryPointBasics(self):
238272
rep = RunEntryPoint()
239273
rep.addOptions()
240-
rep.parse_args([])
274+
rep.parse_args([ARMI_RUN_PATH])
241275

242276
self.assertEqual(rep.name, "run")
243277
self.assertEqual(rep.settingsArgument, "required")

armi/reactor/tests/test_reactors.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,10 @@ def loadTestReactor(
136136
137137
Parameters
138138
----------
139-
inputFilePath : str
139+
inputFilePath : str, default=TEST_ROOT
140140
Path to the directory of the input file.
141141
142-
customSettings : dict with str keys and values of any type
142+
customSettings : dict with str keys and values of any type, default=None
143143
For each key in customSettings, the cs which is loaded from the
144144
armiRun.yaml will be overwritten to the value given in customSettings
145145
for that key.

0 commit comments

Comments
 (0)