diff --git a/k4FWCore/scripts/k4run b/k4FWCore/scripts/k4run index 80d1e37a..1659b01e 100755 --- a/k4FWCore/scripts/k4run +++ b/k4FWCore/scripts/k4run @@ -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 @@ -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", diff --git a/python/k4FWCore/utils.py b/python/k4FWCore/utils.py index 30e91caf..d6fa602d 100644 --- a/python/k4FWCore/utils.py +++ b/python/k4FWCore/utils.py @@ -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: @@ -57,18 +61,17 @@ 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 file object or 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. @@ -77,14 +80,39 @@ def load_file(opt_file: Union[TextIOWrapper, str, os.PathLike]) -> None: Exception: Any exception raised by the executed code will be propagated. """ + # Cannot simply deepcopy globals. Hence, populate the necessary stuff + namespace = { + "__file__": __file__, + "__builtins__": __builtins__, + "__loader__": __loader__, + } + if isinstance(opt_file, (str, os.PathLike)): with open(opt_file, "r") as file: code = file.read() + filename = file.name + + module_name = Path(opt_file).stem + loader = SourceFileLoader(module_name, str(opt_file)) + + namespace.update( + { + "__file__": os.path.realpath(opt_file), + "__spec__": importlib.util.spec_from_loader(loader.name, loader), + } + ) else: + warnings.warn( + "load_file will remove support for handling TextIOWrapper. Please switch to pasing os.PathLike", + FutureWarning, + ) code = opt_file.read() + filename = opt_file.name + namespace.update({"__file__": filename}) check_wrong_imports(str(code)) + code = compile(code, filename, "exec") - exec(code, globals()) + exec(code, namespace) _logger = None diff --git a/test/k4FWCoreTest/CMakeLists.txt b/test/k4FWCoreTest/CMakeLists.txt index 365f68a8..0356b82d 100644 --- a/test/k4FWCoreTest/CMakeLists.txt +++ b/test/k4FWCoreTest/CMakeLists.txt @@ -231,6 +231,17 @@ add_test_with_env(GaudiFunctional options/ExampleGaudiFunctional.py PROPERTIES F add_test_with_env(ReadLimitedInputsIOSvc options/ExampleIOSvcLimitInputCollections.py PROPERTIES FIXTURES_REQUIRED ExampleEventDataFile ADD_TO_CHECK_FILES) add_test_with_env(ReadLimitedInputsAllEventsIOSvc options/ExampleIOSvcLimitInputCollections.py --IOSvc.Output "functional_limited_input_all_events.root" -n -1 PROPERTIES FIXTURES_REQUIRED ExampleEventData 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_with_env(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_with_env(CheckLoadedFileCorrectPathOnError options/checkLoadedFileProperties.py --with-error) +set_tests_properties(CheckLoadedFileCorrectPathOnError + PROPERTIES PASS_REGULAR_EXPRESSION [=[ File ".*/test/k4FWCoreTest/options/checkLoadedFileProperties.py", line 36, in ]=] +) + add_test_with_env(ParticleIDMetadataFramework options/ExampleParticleIDMetadata.py) diff --git a/test/k4FWCoreTest/options/checkLoadedFileProperties.py b/test/k4FWCoreTest/options/checkLoadedFileProperties.py new file mode 100644 index 00000000..64c27c31 --- /dev/null +++ b/test/k4FWCoreTest/options/checkLoadedFileProperties.py @@ -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")