Skip to content
3 changes: 2 additions & 1 deletion k4FWCore/scripts/k4run
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import argparse
import logging
import signal
import warnings
from pathlib import Path

from k4FWCore.utils import load_file, get_logger, set_log_level, LOG_LEVELS

Expand Down Expand Up @@ -141,7 +142,7 @@ def main():

parser.add_argument(
"config_files",
type=open,
type=Path,
action="store",
nargs="*",
help="Gaudi config (python) files describing the job",
Expand Down
43 changes: 27 additions & 16 deletions python/k4FWCore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@
import re
import logging
import sys
from io import TextIOWrapper
from typing import Union
from importlib.machinery import SourceFileLoader
import importlib.util
from pathlib import Path

import warnings


def check_wrong_imports(code: str) -> None:
Expand Down Expand Up @@ -57,18 +61,16 @@ def check_wrong_imports(code: str) -> None:
raise ImportError("Importing ApplicationMgr or IOSvc from Configurables is not allowed.")


def load_file(opt_file: Union[TextIOWrapper, str, os.PathLike]) -> None:
def load_file(opt_file: Union[str, os.PathLike]) -> None:
"""Loads and executes the content of a given file in the current interpreter session.

This function takes a file object or a path to a file, reads its content,
and then executes it as Python code within the global scope of the current
interpreter session. If `opt_file` is a file handle it will not be closed.
This function takes a path to a file, reads its content, and then executes
it as Python code within the global scope of the current interpreter
session.

Args:
opt_file (Union[TextIOWrapper, str, os.PathLike]): A file object or a
path to the file that
contains Python code
to be executed.
opt_file (Union[str, os.PathLike]): A path to the file that contains
Python code to be executed.

Raises:
FileNotFoundError: If `opt_file` is a path and no file exists at that path.
Expand All @@ -77,14 +79,23 @@ def load_file(opt_file: Union[TextIOWrapper, str, os.PathLike]) -> None:
Exception: Any exception raised by the executed code will be propagated.

"""
if isinstance(opt_file, (str, os.PathLike)):
with open(opt_file, "r") as file:
code = file.read()
else:
code = opt_file.read()
check_wrong_imports(str(code))
with open(opt_file, "r") as ofile:
code = ofile.read()

module_name = Path(opt_file).stem
loader = SourceFileLoader(module_name, str(opt_file))

exec(code, globals())
namespace = {
"__file__": os.path.realpath(opt_file),
"__spec__": importlib.util.spec_from_loader(loader.name, loader),
# Cannot simply deepcopy globals. Hence, populate the necessary stuff
"__builtins__": __builtins__,
"__loader__": __loader__,
}

check_wrong_imports(str(code))
code = compile(code, ofile.name, "exec")
exec(code, namespace)


_logger = None
Expand Down
11 changes: 11 additions & 0 deletions test/k4FWCoreTest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,17 @@ add_test_fwcore(GaudiFunctional options/ExampleGaudiFunctional.py PROPERTIES FIX
add_test_fwcore(ReadLimitedInputsIOSvc options/ExampleIOSvcLimitInputCollections.py PROPERTIES FIXTURES_REQUIRED ExampleEventDataFile ADD_TO_CHECK_FILES)
add_test_fwcore(ReadLimitedInputsAllEventsIOSvc options/ExampleIOSvcLimitInputCollections.py --IOSvc.Output "functional_limited_input_all_events.root" -n -1 PROPERTIES FIXTURES_REQUIRED ExampleEventDataFile ADD_TO_CHECK_FILES)

# Tests that ensure that load_file populates the necessary python globals
# accordingly to make loaded files work like imported files
# __file__ points to the loaded file and not k4FWCore/python/utils.py
add_test_fwcore(CheckLoadedFilesHaveCorrectDunderFile options/checkLoadedFileProperties.py)
# If there is an error in the loaded file the filename of that file needs to
# appear in the output
add_test_fwcore(CheckLoadedFileCorrectPathOnError options/checkLoadedFileProperties.py --with-error)
set_tests_properties(CheckLoadedFileCorrectPathOnError
PROPERTIES PASS_REGULAR_EXPRESSION [=[ File ".*/test/k4FWCoreTest/options/checkLoadedFileProperties.py", line 36, in <module>]=]
)

add_test_fwcore(ParticleIDMetadataFramework options/ExampleParticleIDMetadata.py)

add_test_fwcore(EfficiencyFilter options/ExampleEfficiencyFilter.py PROPERTIES DEPENDS EventHeaderFiller FIXTURES_REQUIRED ProducerFile ADD_TO_CHECK_FILES)
Expand Down
38 changes: 38 additions & 0 deletions test/k4FWCoreTest/options/checkLoadedFileProperties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
#
# Copyright (c) 2014-2024 Key4hep-Project.
#
# This file is part of Key4hep.
# See https://key4hep.github.io/key4hep-doc/ for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

# Simple module that ensures that load_file injects the proper information

from k4FWCore.parseArgs import parser


parser.add_argument(
"--with-error",
action="store_true",
default=False,
help="Force a termination due to a syntax error",
)

args = parser.parse_known_args()[0]

if args.with_error:
a = 32 / 0
else:
assert __file__.endswith("checkLoadedFileProperties.py")