Skip to content

Commit 2309ac4

Browse files
Python interface: various PR bugs caught by Copilot
1 parent 8fa1a79 commit 2309ac4

4 files changed

Lines changed: 90 additions & 54 deletions

File tree

sbnalg/gallery/python/LArSoftUtils.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def serviceClassToConfigKeys(serviceClassName: str) -> list[str]:
226226
candidates = []
227227

228228
# remove namespaces (namespaces are not part of any candidate)
229-
try: serviceClassName = serviceClassName[:serviceClassName.index('::')]
229+
try: serviceClassName = serviceClassName[serviceClassName.rindex('::')+2:]
230230
except ValueError: pass # no namespace
231231

232232
if not serviceClassName: return [] # ?!
@@ -284,10 +284,10 @@ def readServiceConfig(getConfig, configKey, returnConfigKey = True):
284284
= tuple(base + suffix for base in serviceClassToConfigKeys(configKey))
285285

286286
Logger.debug("Configuration from candidates: '%s'", "', '".join(configKeys))
287-
for configKey in configKeys:
288-
try: config = getConfig(configKey)
287+
for candidateKey in configKeys:
288+
try: config = getConfig(candidateKey)
289289
except Exception: continue
290-
return (config, configKey) if returnConfigKey else config
290+
return (config, candidateKey) if returnConfigKey else config
291291
raise RuntimeError(f"No configuration for service key '{configKey}'")
292292
# readServiceConfig()
293293

@@ -319,6 +319,7 @@ def loadSimpleService \
319319
config = readServiceConfig(
320320
getConfig=(config.service if config else registry.config),
321321
configKey=serviceName,
322+
returnConfigKey=False,
322323
)
323324
except RuntimeError:
324325
Logger.debug("Full configuration:\n%s",
@@ -400,7 +401,10 @@ def __init__(self,
400401
def _needsSpecialConfig(self): return self.addConfig or self.purgeConfig
401402

402403
def _makeConfig(self, registry):
403-
"""See the constructor for details on the configuration key algorithm."""
404+
"""Fetches, finalizes and returns the FHiCL configuration for the service.
405+
406+
See the constructor for details on the configuration key algorithm.
407+
"""
404408

405409
configKey = self.configKey
406410
if not configKey or configKey.startswith('.'):
@@ -471,16 +475,15 @@ def _loadService(self, manager, dependencies):
471475
The dependencies are expected to be a dictionary: provider name → provider object
472476
(`None` if not available).
473477
"""
474-
475-
# if we don't need a special configuration,
476-
# we let loadSimpleService() find it
478+
479+
# put together the configuration of the service being loaded
477480
registry = manager.registry()
478481
config = self._makeConfig(registry)
479482
Logger.debug("Service configuration:\n%s", config.to_indented_string())
480483

481484
self._loadCode()
482485

483-
# loads the actual classes from ROOT
486+
# load the actual classes from ROOT
484487
self.expandClass('serviceClass')
485488
if self.interfaceClass is not None: self.expandClass('interfaceClass')
486489

@@ -701,6 +704,8 @@ def fullConfig(self): return self.serviceTable is None
701704
def isValid(self): return self.configPath is not None
702705
def hasExtraConfig(self): return bool(self.extraConfig)
703706
def needsCustom(self): return not self.fullConfig() or self.hasExtraConfig()
707+
def serviceTableName(self):
708+
return 'services' if self.fullConfig() else self.serviceTable
704709

705710
def addExtraConfig(self, extra): self.extraConfig += "\n" + extra
706711

@@ -743,7 +748,15 @@ def get(self, serviceKey, interfaceClass = None):
743748
# get()
744749

745750

746-
def defaultConfiguration(self): return None
751+
def defaultConfiguration(self):
752+
"""Returns the default configuration.
753+
754+
The configuration is delivered as a ServiceManagerInstance.ConfigurationInfo object.
755+
This configuration is used when the service manager is not explicitly
756+
configured with `setConfiguration()`.
757+
"""
758+
return ServiceManagerInstance.ConfigurationInfo()
759+
# defaultConfiguration()
747760

748761
def setConfiguration(self, configFile, serviceTable = None, extra = ""):
749762
"""Sets which configuration to use for setup.
@@ -787,13 +800,18 @@ def loadConfiguration(self):
787800
else self.defaultConfiguration()
788801

789802
# if assertion fails, then `setConfiguration()` was not correctly called.
790-
assert configurationInfo.isValid()
803+
if not configurationInfo or not configurationInfo.isValid():
804+
raise RuntimeError(
805+
"No configuration in place at setup time. Call 'setConfiguration()'"
806+
" or override 'defaultConfiguration()' to provide a configuration file path."
807+
)
808+
# if
791809

792810
if configurationInfo.needsCustom():
793811
Logger.debug(
794812
"Using service table '%s' from configuration file '%s' plus %d custom lines",
795-
configurationInfo.serviceTable, configurationInfo.configPath,
796-
sum(c == '\n' for c in configurationInfo.extraConfig),
813+
configurationInfo.serviceTableName(), configurationInfo.configPath,
814+
sum(c == '\n' for c in configurationInfo.extraConfig) + 1,
797815
)
798816
config = galleryUtils.ConfigurationString(
799817
'#include "{configPath}"'
@@ -811,7 +829,7 @@ def loadConfiguration(self):
811829
'\n# ==============================='
812830
.format(
813831
configPath=configurationInfo.configPath,
814-
serviceTable=configurationInfo.serviceTable,
832+
serviceTable=configurationInfo.serviceTableName(),
815833
extraConfig=configurationInfo.extraConfig,
816834
)
817835
)

sbnalg/gallery/python/ROOTutils.py

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -262,14 +262,14 @@ def getROOTclass(classPath):
262262
################################################################################
263263
# this is not really specific to ROOT, but we often have ROOT file lists
264264
def expandFileList(
265-
fileListPaths: "path (or paths) of the file list (or single file)",
265+
dataPaths: "path (or paths) of the file list (or single file)",
266266
comment: "(default: '#') character used to introduce a comment" = '#',
267-
fileListSuffixes: "suffix of entries to recursively add file lists" = [],
268-
fileSuffixes: "suffix of entries never to be treated as file lists" = [ '.root' ],
267+
fileListSuffixes: "suffix of entries to recursively add file lists" = (),
268+
fileSuffixes: "suffix of entries never to be treated as file lists" = ( '.root', ),
269269
) -> "a list of file names":
270270
"""Returns a list of file names as found in the specified file lists.
271271
272-
The `fileListPaths` paths are read as text files; in them, each line
272+
The `dataPaths` paths are read as text files; in them, each line
273273
represents a full file path.
274274
Empty lines and lines starting with a comment character are ignored.
275275
Also if blanks and a comment character are found, the content of the line
@@ -281,13 +281,16 @@ def expandFileList(
281281
If file suffixes are specified, a line ending with any of those suffixes
282282
will be considered a file, and not expanded. That takes priority over the file
283283
list suffix.
284+
Only absolute paths are supported in file lists. The current behaviour,
285+
which does not reject relative paths and assumes them relative to the current
286+
directory, is not guaranteed and may change in the future (ideally, it will).
284287
285-
If any of the file lists in `fileListPaths` can't be read, an exception is
288+
If any of the file lists in `dataPaths` can't be read, an exception is
286289
raised.
287290
288-
NOTE: because of the internal implementation, a `fileListPaths` that is a
291+
NOTE: because of the internal implementation, a `dataPaths` that is a
289292
collection of file names that are all one character long may be mistaken
290-
for a string. The general solution is: do not name your file not file lists
293+
for a string. The general solution is: do not name your file nor file lists
291294
with a single-character name. It's most often a bad idea anyway.
292295
"""
293296

@@ -297,48 +300,65 @@ def expandFileList(
297300
)
298301

299302
# ----------------------------------------------------------------------------
300-
# Path or list?
303+
# Path or collection of paths?
301304
#
302-
# This looks very silly, but distinguishing a string and a list is not trivial
303-
# (even less so in Python); and we need to support both std::vector and list,
304-
# both str and std::string.
305+
# This looks very silly, but distinguishing a string and a collection of
306+
# strings is not trivial (even less so in Python); and we need to support both
307+
# std::vector and list, both str and std::string.
305308
# The elements of all these types are of Python type str.
306309
# So it is hereby decided that if they all are of length 1, then it's a string
307-
# otherwise it is a list. Yes, this fails in an obvious corner case.
308-
# Just don't call your file lists "a", "b" etc. Nor your files.
310+
# otherwise it is a collection. Yes, this fails in an obvious corner case.
311+
# Just don't call your files or file lists "a", "b" etc.
309312
# Nevertheless, some special cases are singled out and specifically addressed.
310313

311-
isClearlyList = isinstance(fileListPaths, (ROOT.std.vector[ROOT.std.string], list, set))
312-
isClearlyPath = isinstance(fileListPaths, (ROOT.std.string, str))
314+
isClearlyColl = isinstance(dataPaths, (ROOT.std.vector[ROOT.std.string], list, tuple, set))
315+
isClearlyPath = isinstance(dataPaths, (ROOT.std.string, str))
313316
# we expand the argument to support single-pass generators; what happens?
314317
# str -> list[str] (one character each element)
315318
# std::string -> [str ->] list[str] (one character each element)
316319
# Python iterable -> list (usually?)
317320
# Python generator -> list
318-
fileListList = list(fileListPaths)
319-
if isClearlyList or (any(len(p) > 1 for p in fileListList) and not isClearlyPath):
321+
dataPathList = list(dataPaths)
322+
if isClearlyColl or (any(len(p) > 1 for p in dataPathList) and not isClearlyPath):
320323
Logger.debug("Input is a list of %d paths and will be expanded as such.",
321-
len(fileListList))
322-
return sum([ expandAgain(path) for path in fileListList ], [])
324+
len(dataPathList))
325+
return sum([ expandAgain(path) for path in dataPathList ], [])
323326
# if the argument is a list
324327

325-
# at this point we believe the original argument was a string (not a list,
326-
# not a generator) and we broke it into pieces into `fileListList`.
327-
# But `fileListPath` is still whole, so we will use it.
328+
# at this point we believe the original argument was a string (not a
329+
# collection nor generator) and we broke it into pieces into `dataPaths`.
330+
# But `dataPaths` is still whole, so we will use it.
328331

329332
# ----------------------------------------------------------------------------
330333
# Expand, at last
331334
#
332-
# from here on, it's only one file or file list
333-
fileListPath = fileListPaths
335+
# from here on, it's only a single path (file or file list)
336+
#
337+
# NOTE on relative paths: in principle we can expand relative paths prepending
338+
# the base path of the parent file list (if any; otherwise we can either
339+
# append the current directory, or leave them relative).
340+
# One complication is that paths may not be easy to identify as relative;
341+
# for example, a XRootD URL is considered by `os.path.isabs()` as relative.
342+
# Here `pathlib` module may help. Another complication is if a base path
343+
# includes symbolic links. Imagine an `output` directory structure with
344+
# `output/data/` and `output/lists/`, where the items of the file lists in
345+
# the latter all include a `../data` path. While that list works well when
346+
# expanded using its real path (`output/lists/../data/...`), when such list
347+
# is linked somewhere else, the simple expansion of those files to
348+
# `<current dir>/../data/...` will be broken. In addition, the use of
349+
# file-system-accessing functions as `os.path.realpath()` may still break
350+
# since those functions won't work paths like XRootD URL.
351+
# All of this can be worked around, with enough motivation.
352+
#
353+
dataPath = dataPaths
334354
hasSuffix = lambda s, suffixes: any(s.endswith(sx) for sx in suffixes)
335355

336-
if hasSuffix(fileListPath, fileSuffixes):
337-
Logger.debug("'%s' was for sure a file, not a file list.", fileListPath)
338-
return [ fileListPath ]
356+
if hasSuffix(dataPath, fileSuffixes):
357+
Logger.debug("'%s' was for sure a file, not a file list.", dataPath)
358+
return [ dataPath ]
339359

340-
Logger.debug("Processing file list '%s'", fileListPath)
341-
with open(fileListPath, 'r') as fileList:
360+
Logger.debug("Processing file list '%s'", dataPath)
361+
with open(dataPath, 'r') as fileList:
342362

343363
l = []
344364
for iLine, line in enumerate(fileList):

sbnalg/gallery/python/cppUtils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,13 @@ def loadMany(self, *pathSpecs, extraPaths = [], force = False, loadAll = False):
145145
"""Calls `load()` for each of the path specifications.
146146
147147
A path specification is a dictionary of `load()` parameters, e.g.
148-
`{ relPath: 'larcorealg_Geometry', extraPaths: [] }`.
148+
`dict(relPath='larcorealg_Geometry', extraPaths=[])`.
149149
If a path specification is not a dictionary, it is assumed to be a `relPath`
150-
and it is equivalent to having `{ relPath: pathSpec }`.
151-
If an argument `extraPath` or `force` is specified in the path
152-
specification, a value is used from the `extraPath` and `force` arguments
153-
of this function.
150+
and it is equivalent to having `dict(relPath=pathSpec)`.
151+
If `extraPaths` or `force` are not specified in a path specification,
152+
they default to the `extraPaths` and `force` arguments of this function;
153+
if they are specified in the path specification, their values override
154+
the defaults from this function.
154155
155156
If `loadAll` is set, all loads are attempted; the return value is a pair:
156157
whether an exception was thrown, and a list of all return values or

sbnalg/gallery/python/galleryUtils.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,8 @@ def eventLoop(inputFiles,
289289
nSkip = options.get('nSkip', 0)
290290
nEvents = options.get('nEvents', None)
291291

292-
# make sure the input file list is in the right format
293-
if not isinstance(inputFiles, ROOT.vector(ROOT.string)):
294-
if isinstance(inputFiles, str): inputFiles = [ inputFiles, ]
295-
inputFiles = makeFileList(*inputFiles)
296-
# if
292+
# create a gallery event with an expanded (flattened) input list
293+
inputFiles = makeFileList(inputFiles)
297294

298295
event = ROOT.gallery.Event(inputFiles)
299296

@@ -533,7 +530,7 @@ def init(self, config, applName):
533530
if not applName: applName = os.path.basename(sys.argv[0])
534531
if not applName: applName = "this application"
535532
if isinstance(config, ConfigurationClass): config = config.service("message")
536-
Logger.debug("Starting message facility for {%s}...", applName)
533+
Logger.debug("Starting message facility for %s...", applName)
537534
try:
538535
ROOT.mf.StartMessageFacility(config, applName)
539536
except ROOT.std.exception:

0 commit comments

Comments
 (0)