Coverage for python/lsst/ctrl/mpexec/cmdLineFwk.py: 15%

278 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-04 02:33 -0700

1# This file is part of ctrl_mpexec. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22"""Module defining CmdLineFwk class and related methods. 

23""" 

24 

25from __future__ import annotations 

26 

27__all__ = ["CmdLineFwk"] 

28 

29# ------------------------------- 

30# Imports of standard modules -- 

31# ------------------------------- 

32import copy 

33import datetime 

34import getpass 

35import logging 

36from collections.abc import Iterable, Sequence 

37from types import SimpleNamespace 

38from typing import Optional, Tuple 

39 

40from lsst.daf.butler import Butler, CollectionType, DatasetRef, Registry 

41from lsst.daf.butler.registry import MissingCollectionError, RegistryDefaults 

42from lsst.daf.butler.registry.wildcards import CollectionWildcard 

43from lsst.pipe.base import ( 

44 GraphBuilder, 

45 Instrument, 

46 Pipeline, 

47 PipelineDatasetTypes, 

48 QuantumGraph, 

49 TaskDef, 

50 TaskFactory, 

51 buildExecutionButler, 

52) 

53from lsst.utils import doImportType 

54 

55from . import util 

56from .dotTools import graph2dot, pipeline2dot 

57from .executionGraphFixup import ExecutionGraphFixup 

58from .mpGraphExecutor import MPGraphExecutor 

59from .preExecInit import PreExecInit 

60from .singleQuantumExecutor import SingleQuantumExecutor 

61 

62# ---------------------------------- 

63# Local non-exported definitions -- 

64# ---------------------------------- 

65 

66_LOG = logging.getLogger(__name__) 

67 

68 

69class _OutputChainedCollectionInfo: 

70 """A helper class for handling command-line arguments related to an output 

71 `~lsst.daf.butler.CollectionType.CHAINED` collection. 

72 

73 Parameters 

74 ---------- 

75 registry : `lsst.daf.butler.Registry` 

76 Butler registry that collections will be added to and/or queried from. 

77 name : `str` 

78 Name of the collection given on the command line. 

79 """ 

80 

81 def __init__(self, registry: Registry, name: str): 

82 self.name = name 

83 try: 

84 self.chain = tuple(registry.getCollectionChain(name)) 

85 self.exists = True 

86 except MissingCollectionError: 

87 self.chain = () 

88 self.exists = False 

89 

90 def __str__(self) -> str: 

91 return self.name 

92 

93 name: str 

94 """Name of the collection provided on the command line (`str`). 

95 """ 

96 

97 exists: bool 

98 """Whether this collection already exists in the registry (`bool`). 

99 """ 

100 

101 chain: Tuple[str, ...] 

102 """The definition of the collection, if it already exists (`tuple`[`str`]). 

103 

104 Empty if the collection does not already exist. 

105 """ 

106 

107 

108class _OutputRunCollectionInfo: 

109 """A helper class for handling command-line arguments related to an output 

110 `~lsst.daf.butler.CollectionType.RUN` collection. 

111 

112 Parameters 

113 ---------- 

114 registry : `lsst.daf.butler.Registry` 

115 Butler registry that collections will be added to and/or queried from. 

116 name : `str` 

117 Name of the collection given on the command line. 

118 """ 

119 

120 def __init__(self, registry: Registry, name: str): 

121 self.name = name 

122 try: 

123 actualType = registry.getCollectionType(name) 

124 if actualType is not CollectionType.RUN: 

125 raise TypeError(f"Collection '{name}' exists but has type {actualType.name}, not RUN.") 

126 self.exists = True 

127 except MissingCollectionError: 

128 self.exists = False 

129 

130 name: str 

131 """Name of the collection provided on the command line (`str`). 

132 """ 

133 

134 exists: bool 

135 """Whether this collection already exists in the registry (`bool`). 

136 """ 

137 

138 

139class _ButlerFactory: 

140 """A helper class for processing command-line arguments related to input 

141 and output collections. 

142 

143 Parameters 

144 ---------- 

145 registry : `lsst.daf.butler.Registry` 

146 Butler registry that collections will be added to and/or queried from. 

147 

148 args : `types.SimpleNamespace` 

149 Parsed command-line arguments. The following attributes are used, 

150 either at construction or in later methods. 

151 

152 ``output`` 

153 The name of a `~lsst.daf.butler.CollectionType.CHAINED` 

154 input/output collection. 

155 

156 ``output_run`` 

157 The name of a `~lsst.daf.butler.CollectionType.RUN` input/output 

158 collection. 

159 

160 ``extend_run`` 

161 A boolean indicating whether ``output_run`` should already exist 

162 and be extended. 

163 

164 ``replace_run`` 

165 A boolean indicating that (if `True`) ``output_run`` should already 

166 exist but will be removed from the output chained collection and 

167 replaced with a new one. 

168 

169 ``prune_replaced`` 

170 A boolean indicating whether to prune the replaced run (requires 

171 ``replace_run``). 

172 

173 ``inputs`` 

174 Input collections of any type; see 

175 :ref:`daf_butler_ordered_collection_searches` for details. 

176 

177 ``butler_config`` 

178 Path to a data repository root or configuration file. 

179 

180 writeable : `bool` 

181 If `True`, a `Butler` is being initialized in a context where actual 

182 writes should happens, and hence no output run is necessary. 

183 

184 Raises 

185 ------ 

186 ValueError 

187 Raised if ``writeable is True`` but there are no output collections. 

188 """ 

189 

190 def __init__(self, registry: Registry, args: SimpleNamespace, writeable: bool): 

191 if args.output is not None: 

192 self.output = _OutputChainedCollectionInfo(registry, args.output) 

193 else: 

194 self.output = None 

195 if args.output_run is not None: 

196 self.outputRun = _OutputRunCollectionInfo(registry, args.output_run) 

197 elif self.output is not None: 

198 if args.extend_run: 

199 if not self.output.chain: 

200 raise ValueError("Cannot use --extend-run option with non-existing or empty output chain") 

201 runName = self.output.chain[0] 

202 else: 

203 runName = "{}/{}".format(self.output, Instrument.makeCollectionTimestamp()) 

204 self.outputRun = _OutputRunCollectionInfo(registry, runName) 

205 elif not writeable: 

206 # If we're not writing yet, ok to have no output run. 

207 self.outputRun = None 

208 else: 

209 raise ValueError("Cannot write without at least one of (--output, --output-run).") 

210 # Recursively flatten any input CHAINED collections. We do this up 

211 # front so we can tell if the user passes the same inputs on subsequent 

212 # calls, even though we also flatten when we define the output CHAINED 

213 # collection. 

214 self.inputs = tuple(registry.queryCollections(args.input, flattenChains=True)) if args.input else () 

215 

216 def check(self, args: SimpleNamespace) -> None: 

217 """Check command-line options for consistency with each other and the 

218 data repository. 

219 

220 Parameters 

221 ---------- 

222 args : `types.SimpleNamespace` 

223 Parsed command-line arguments. See class documentation for the 

224 construction parameter of the same name. 

225 """ 

226 assert not (args.extend_run and args.replace_run), "In mutually-exclusive group in ArgumentParser." 

227 if self.inputs and self.output is not None and self.output.exists: 

228 # Passing the same inputs that were used to initialize the output 

229 # collection is allowed; this means they must _end_ with the same 

230 # collections, because we push new runs to the front of the chain. 

231 for c1, c2 in zip(self.inputs[::-1], self.output.chain[::-1]): 

232 if c1 != c2: 

233 raise ValueError( 

234 f"Output CHAINED collection {self.output.name!r} exists, but it ends with " 

235 "a different sequence of input collections than those given: " 

236 f"{c1!r} != {c2!r} in inputs={self.inputs} vs " 

237 f"{self.output.name}={self.output.chain}." 

238 ) 

239 if len(self.inputs) > len(self.output.chain): 

240 nNew = len(self.inputs) - len(self.output.chain) 

241 raise ValueError( 

242 f"Cannot add new input collections {self.inputs[:nNew]} after " 

243 "output collection is first created." 

244 ) 

245 if args.extend_run: 

246 if self.outputRun is None: 

247 raise ValueError("Cannot --extend-run when no output collection is given.") 

248 elif not self.outputRun.exists: 

249 raise ValueError( 

250 f"Cannot --extend-run; output collection '{self.outputRun.name}' does not exist." 

251 ) 

252 if not args.extend_run and self.outputRun is not None and self.outputRun.exists: 

253 raise ValueError( 

254 f"Output run '{self.outputRun.name}' already exists, but --extend-run was not given." 

255 ) 

256 if args.prune_replaced and not args.replace_run: 

257 raise ValueError("--prune-replaced requires --replace-run.") 

258 if args.replace_run and (self.output is None or not self.output.exists): 

259 raise ValueError("--output must point to an existing CHAINED collection for --replace-run.") 

260 

261 @classmethod 

262 def _makeReadParts(cls, args: SimpleNamespace) -> tuple[Butler, Sequence[str], _ButlerFactory]: 

263 """Common implementation for `makeReadButler` and 

264 `makeButlerAndCollections`. 

265 

266 Parameters 

267 ---------- 

268 args : `types.SimpleNamespace` 

269 Parsed command-line arguments. See class documentation for the 

270 construction parameter of the same name. 

271 

272 Returns 

273 ------- 

274 butler : `lsst.daf.butler.Butler` 

275 A read-only butler constructed from the repo at 

276 ``args.butler_config``, but with no default collections. 

277 inputs : `Sequence` [ `str` ] 

278 A collection search path constructed according to ``args``. 

279 self : `_ButlerFactory` 

280 A new `_ButlerFactory` instance representing the processed version 

281 of ``args``. 

282 """ 

283 butler = Butler(args.butler_config, writeable=False) 

284 self = cls(butler.registry, args, writeable=False) 

285 self.check(args) 

286 if self.output and self.output.exists: 

287 if args.replace_run: 

288 replaced = self.output.chain[0] 

289 inputs = list(self.output.chain[1:]) 

290 _LOG.debug( 

291 "Simulating collection search in '%s' after removing '%s'.", self.output.name, replaced 

292 ) 

293 else: 

294 inputs = [self.output.name] 

295 else: 

296 inputs = list(self.inputs) 

297 if args.extend_run: 

298 assert self.outputRun is not None, "Output collection has to be specified." 

299 inputs.insert(0, self.outputRun.name) 

300 collSearch = CollectionWildcard.from_expression(inputs).require_ordered() 

301 return butler, collSearch, self 

302 

303 @classmethod 

304 def makeReadButler(cls, args: SimpleNamespace) -> Butler: 

305 """Construct a read-only butler according to the given command-line 

306 arguments. 

307 

308 Parameters 

309 ---------- 

310 args : `types.SimpleNamespace` 

311 Parsed command-line arguments. See class documentation for the 

312 construction parameter of the same name. 

313 

314 Returns 

315 ------- 

316 butler : `lsst.daf.butler.Butler` 

317 A read-only butler initialized with the collections specified by 

318 ``args``. 

319 """ 

320 butler, inputs, _ = cls._makeReadParts(args) 

321 _LOG.debug("Preparing butler to read from %s.", inputs) 

322 return Butler(butler=butler, collections=inputs) 

323 

324 @classmethod 

325 def makeButlerAndCollections(cls, args: SimpleNamespace) -> Tuple[Butler, Sequence[str], Optional[str]]: 

326 """Return a read-only registry, a collection search path, and the name 

327 of the run to be used for future writes. 

328 

329 Parameters 

330 ---------- 

331 args : `types.SimpleNamespace` 

332 Parsed command-line arguments. See class documentation for the 

333 construction parameter of the same name. 

334 

335 Returns 

336 ------- 

337 butler : `lsst.daf.butler.Butler` 

338 A read-only butler that collections will be added to and/or queried 

339 from. 

340 inputs : `Sequence` [ `str` ] 

341 Collections to search for datasets. 

342 run : `str` or `None` 

343 Name of the output `~lsst.daf.butler.CollectionType.RUN` collection 

344 if it already exists, or `None` if it does not. 

345 """ 

346 butler, inputs, self = cls._makeReadParts(args) 

347 run: Optional[str] = None 

348 if args.extend_run: 

349 assert self.outputRun is not None, "Output collection has to be specified." 

350 run = self.outputRun.name 

351 _LOG.debug("Preparing registry to read from %s and expect future writes to '%s'.", inputs, run) 

352 return butler, inputs, run 

353 

354 @classmethod 

355 def makeWriteButler(cls, args: SimpleNamespace, taskDefs: Optional[Iterable[TaskDef]] = None) -> Butler: 

356 """Return a read-write butler initialized to write to and read from 

357 the collections specified by the given command-line arguments. 

358 

359 Parameters 

360 ---------- 

361 args : `types.SimpleNamespace` 

362 Parsed command-line arguments. See class documentation for the 

363 construction parameter of the same name. 

364 taskDefs : iterable of `TaskDef`, optional 

365 Definitions for tasks in a pipeline. This argument is only needed 

366 if ``args.replace_run`` is `True` and ``args.prune_replaced`` is 

367 "unstore". 

368 

369 Returns 

370 ------- 

371 butler : `lsst.daf.butler.Butler` 

372 A read-write butler initialized according to the given arguments. 

373 """ 

374 butler = Butler(args.butler_config, writeable=True) 

375 self = cls(butler.registry, args, writeable=True) 

376 self.check(args) 

377 assert self.outputRun is not None, "Output collection has to be specified." # for mypy 

378 if self.output is not None: 

379 chainDefinition = list(self.output.chain if self.output.exists else self.inputs) 

380 if args.replace_run: 

381 replaced = chainDefinition.pop(0) 

382 if args.prune_replaced == "unstore": 

383 # Remove datasets from datastore 

384 with butler.transaction(): 

385 refs: Iterable[DatasetRef] = butler.registry.queryDatasets(..., collections=replaced) 

386 # we want to remove regular outputs but keep 

387 # initOutputs, configs, and versions. 

388 if taskDefs is not None: 

389 initDatasetNames = set(PipelineDatasetTypes.initOutputNames(taskDefs)) 

390 refs = [ref for ref in refs if ref.datasetType.name not in initDatasetNames] 

391 butler.pruneDatasets(refs, unstore=True, disassociate=False) 

392 elif args.prune_replaced == "purge": 

393 # Erase entire collection and all datasets, need to remove 

394 # collection from its chain collection first. 

395 with butler.transaction(): 

396 butler.registry.setCollectionChain(self.output.name, chainDefinition, flatten=True) 

397 butler.pruneCollection(replaced, purge=True, unstore=True) 

398 elif args.prune_replaced is not None: 

399 raise NotImplementedError(f"Unsupported --prune-replaced option '{args.prune_replaced}'.") 

400 if not self.output.exists: 

401 butler.registry.registerCollection(self.output.name, CollectionType.CHAINED) 

402 if not args.extend_run: 

403 butler.registry.registerCollection(self.outputRun.name, CollectionType.RUN) 

404 chainDefinition.insert(0, self.outputRun.name) 

405 butler.registry.setCollectionChain(self.output.name, chainDefinition, flatten=True) 

406 _LOG.debug( 

407 "Preparing butler to write to '%s' and read from '%s'=%s", 

408 self.outputRun.name, 

409 self.output.name, 

410 chainDefinition, 

411 ) 

412 butler.registry.defaults = RegistryDefaults(run=self.outputRun.name, collections=self.output.name) 

413 else: 

414 inputs = (self.outputRun.name,) + self.inputs 

415 _LOG.debug("Preparing butler to write to '%s' and read from %s.", self.outputRun.name, inputs) 

416 butler.registry.defaults = RegistryDefaults(run=self.outputRun.name, collections=inputs) 

417 return butler 

418 

419 output: Optional[_OutputChainedCollectionInfo] 

420 """Information about the output chained collection, if there is or will be 

421 one (`_OutputChainedCollectionInfo` or `None`). 

422 """ 

423 

424 outputRun: Optional[_OutputRunCollectionInfo] 

425 """Information about the output run collection, if there is or will be 

426 one (`_OutputRunCollectionInfo` or `None`). 

427 """ 

428 

429 inputs: Tuple[str, ...] 

430 """Input collections provided directly by the user (`tuple` [ `str` ]). 

431 """ 

432 

433 

434# ------------------------ 

435# Exported definitions -- 

436# ------------------------ 

437 

438 

439class CmdLineFwk: 

440 """PipelineTask framework which executes tasks from command line. 

441 

442 In addition to executing tasks this activator provides additional methods 

443 for task management like dumping configuration or execution chain. 

444 """ 

445 

446 MP_TIMEOUT = 3600 * 24 * 30 # Default timeout (sec) for multiprocessing 

447 

448 def __init__(self) -> None: 

449 pass 

450 

451 def makePipeline(self, args: SimpleNamespace) -> Pipeline: 

452 """Build a pipeline from command line arguments. 

453 

454 Parameters 

455 ---------- 

456 args : `types.SimpleNamespace` 

457 Parsed command line 

458 

459 Returns 

460 ------- 

461 pipeline : `~lsst.pipe.base.Pipeline` 

462 """ 

463 if args.pipeline: 

464 pipeline = Pipeline.from_uri(args.pipeline) 

465 else: 

466 pipeline = Pipeline("anonymous") 

467 

468 # loop over all pipeline actions and apply them in order 

469 for action in args.pipeline_actions: 

470 if action.action == "add_instrument": 

471 

472 pipeline.addInstrument(action.value) 

473 

474 elif action.action == "new_task": 

475 

476 pipeline.addTask(action.value, action.label) 

477 

478 elif action.action == "delete_task": 

479 

480 pipeline.removeTask(action.label) 

481 

482 elif action.action == "config": 

483 

484 # action value string is "field=value", split it at '=' 

485 field, _, value = action.value.partition("=") 

486 pipeline.addConfigOverride(action.label, field, value) 

487 

488 elif action.action == "configfile": 

489 

490 pipeline.addConfigFile(action.label, action.value) 

491 

492 else: 

493 

494 raise ValueError(f"Unexpected pipeline action: {action.action}") 

495 

496 if args.save_pipeline: 

497 pipeline.write_to_uri(args.save_pipeline) 

498 

499 if args.pipeline_dot: 

500 pipeline2dot(pipeline, args.pipeline_dot) 

501 

502 return pipeline 

503 

504 def makeGraph(self, pipeline: Pipeline, args: SimpleNamespace) -> Optional[QuantumGraph]: 

505 """Build a graph from command line arguments. 

506 

507 Parameters 

508 ---------- 

509 pipeline : `~lsst.pipe.base.Pipeline` 

510 Pipeline, can be empty or ``None`` if graph is read from a file. 

511 args : `types.SimpleNamespace` 

512 Parsed command line 

513 

514 Returns 

515 ------- 

516 graph : `~lsst.pipe.base.QuantumGraph` or `None` 

517 If resulting graph is empty then `None` is returned. 

518 """ 

519 

520 # make sure that --extend-run always enables --skip-existing 

521 if args.extend_run: 

522 args.skip_existing = True 

523 

524 butler, collections, run = _ButlerFactory.makeButlerAndCollections(args) 

525 

526 if args.skip_existing and run: 

527 args.skip_existing_in += (run,) 

528 

529 if args.qgraph: 

530 # click passes empty tuple as default value for qgraph_node_id 

531 nodes = args.qgraph_node_id or None 

532 qgraph = QuantumGraph.loadUri( 

533 args.qgraph, butler.registry.dimensions, nodes=nodes, graphID=args.qgraph_id 

534 ) 

535 

536 # pipeline can not be provided in this case 

537 if pipeline: 

538 raise ValueError("Pipeline must not be given when quantum graph is read from file.") 

539 if args.show_qgraph_header: 

540 print(QuantumGraph.readHeader(args.qgraph)) 

541 else: 

542 # make execution plan (a.k.a. DAG) for pipeline 

543 graphBuilder = GraphBuilder( 

544 butler.registry, 

545 skipExistingIn=args.skip_existing_in, 

546 clobberOutputs=args.clobber_outputs, 

547 datastore=butler.datastore if args.qgraph_datastore_records else None, 

548 ) 

549 # accumulate metadata 

550 metadata = { 

551 "input": args.input, 

552 "output": args.output, 

553 "butler_argument": args.butler_config, 

554 "output_run": args.output_run, 

555 "extend_run": args.extend_run, 

556 "skip_existing_in": args.skip_existing_in, 

557 "skip_existing": args.skip_existing, 

558 "data_query": args.data_query, 

559 "user": getpass.getuser(), 

560 "time": f"{datetime.datetime.now()}", 

561 } 

562 qgraph = graphBuilder.makeGraph( 

563 pipeline, 

564 collections, 

565 run, 

566 args.data_query, 

567 metadata=metadata, 

568 datasetQueryConstraint=args.dataset_query_constraint, 

569 ) 

570 if args.show_qgraph_header: 

571 qgraph.buildAndPrintHeader() 

572 

573 # Count quanta in graph and give a warning if it's empty and return 

574 # None. 

575 nQuanta = len(qgraph) 

576 if nQuanta == 0: 

577 return None 

578 else: 

579 _LOG.info( 

580 "QuantumGraph contains %d quanta for %d tasks, graph ID: %r", 

581 nQuanta, 

582 len(qgraph.taskGraph), 

583 qgraph.graphID, 

584 ) 

585 

586 if args.save_qgraph: 

587 qgraph.saveUri(args.save_qgraph) 

588 

589 if args.save_single_quanta: 

590 for quantumNode in qgraph: 

591 sqgraph = qgraph.subset(quantumNode) 

592 uri = args.save_single_quanta.format(quantumNode) 

593 sqgraph.saveUri(uri) 

594 

595 if args.qgraph_dot: 

596 graph2dot(qgraph, args.qgraph_dot) 

597 

598 if args.execution_butler_location: 

599 butler = Butler(args.butler_config) 

600 newArgs = copy.deepcopy(args) 

601 

602 def builderShim(butler: Butler) -> Butler: 

603 newArgs.butler_config = butler._config 

604 # Calling makeWriteButler is done for the side effects of 

605 # calling that method, maining parsing all the args into 

606 # collection names, creating collections, etc. 

607 newButler = _ButlerFactory.makeWriteButler(newArgs) 

608 return newButler 

609 

610 # Include output collection in collections for input 

611 # files if it exists in the repo. 

612 all_inputs = args.input 

613 if args.output is not None: 

614 try: 

615 all_inputs += (next(iter(butler.registry.queryCollections(args.output))),) 

616 except MissingCollectionError: 

617 pass 

618 

619 _LOG.debug("Calling buildExecutionButler with collections=%s", all_inputs) 

620 buildExecutionButler( 

621 butler, 

622 qgraph, 

623 args.execution_butler_location, 

624 run, 

625 butlerModifier=builderShim, 

626 collections=all_inputs, 

627 clobber=args.clobber_execution_butler, 

628 datastoreRoot=args.target_datastore_root, 

629 transfer=args.transfer, 

630 ) 

631 

632 return qgraph 

633 

634 def runPipeline( 

635 self, 

636 graph: QuantumGraph, 

637 taskFactory: TaskFactory, 

638 args: SimpleNamespace, 

639 butler: Optional[Butler] = None, 

640 ) -> None: 

641 """Execute complete QuantumGraph. 

642 

643 Parameters 

644 ---------- 

645 graph : `QuantumGraph` 

646 Execution graph. 

647 taskFactory : `~lsst.pipe.base.TaskFactory` 

648 Task factory 

649 args : `types.SimpleNamespace` 

650 Parsed command line 

651 butler : `~lsst.daf.butler.Butler`, optional 

652 Data Butler instance, if not defined then new instance is made 

653 using command line options. 

654 """ 

655 # make sure that --extend-run always enables --skip-existing 

656 if args.extend_run: 

657 args.skip_existing = True 

658 

659 # make butler instance 

660 if butler is None: 

661 butler = _ButlerFactory.makeWriteButler(args, graph.iterTaskGraph()) 

662 

663 if args.skip_existing: 

664 args.skip_existing_in += (butler.run,) 

665 

666 # Enable lsstDebug debugging. Note that this is done once in the 

667 # main process before PreExecInit and it is also repeated before 

668 # running each task in SingleQuantumExecutor (which may not be 

669 # needed if `multipocessing` always uses fork start method). 

670 if args.enableLsstDebug: 

671 try: 

672 _LOG.debug("Will try to import debug.py") 

673 import debug # type: ignore # noqa:F401 

674 except ImportError: 

675 _LOG.warn("No 'debug' module found.") 

676 

677 # Save all InitOutputs, configs, etc. 

678 preExecInit = PreExecInit(butler, taskFactory, extendRun=args.extend_run, mock=args.mock) 

679 preExecInit.initialize( 

680 graph, 

681 saveInitOutputs=not args.skip_init_writes, 

682 registerDatasetTypes=args.register_dataset_types, 

683 saveVersions=not args.no_versions, 

684 ) 

685 

686 if not args.init_only: 

687 graphFixup = self._importGraphFixup(args) 

688 quantumExecutor = SingleQuantumExecutor( 

689 taskFactory, 

690 skipExistingIn=args.skip_existing_in, 

691 clobberOutputs=args.clobber_outputs, 

692 enableLsstDebug=args.enableLsstDebug, 

693 exitOnKnownError=args.fail_fast, 

694 mock=args.mock, 

695 mock_configs=args.mock_configs, 

696 ) 

697 timeout = self.MP_TIMEOUT if args.timeout is None else args.timeout 

698 executor = MPGraphExecutor( 

699 numProc=args.processes, 

700 timeout=timeout, 

701 startMethod=args.start_method, 

702 quantumExecutor=quantumExecutor, 

703 failFast=args.fail_fast, 

704 pdb=args.pdb, 

705 executionGraphFixup=graphFixup, 

706 ) 

707 try: 

708 with util.profile(args.profile, _LOG): 

709 executor.execute(graph, butler) 

710 finally: 

711 if args.summary: 

712 report = executor.getReport() 

713 if report: 

714 with open(args.summary, "w") as out: 

715 # Do not save fields that are not set. 

716 out.write(report.json(exclude_none=True, indent=2)) 

717 

718 def _importGraphFixup(self, args: SimpleNamespace) -> Optional[ExecutionGraphFixup]: 

719 """Import/instantiate graph fixup object. 

720 

721 Parameters 

722 ---------- 

723 args : `types.SimpleNamespace` 

724 Parsed command line. 

725 

726 Returns 

727 ------- 

728 fixup : `ExecutionGraphFixup` or `None` 

729 

730 Raises 

731 ------ 

732 ValueError 

733 Raised if import fails, method call raises exception, or returned 

734 instance has unexpected type. 

735 """ 

736 if args.graph_fixup: 

737 try: 

738 factory = doImportType(args.graph_fixup) 

739 except Exception as exc: 

740 raise ValueError("Failed to import graph fixup class/method") from exc 

741 try: 

742 fixup = factory() 

743 except Exception as exc: 

744 raise ValueError("Failed to make instance of graph fixup") from exc 

745 if not isinstance(fixup, ExecutionGraphFixup): 

746 raise ValueError("Graph fixup is not an instance of ExecutionGraphFixup class") 

747 return fixup 

748 return None