Skip to content

Populate correct meta information in load_file, deprecate taking file objects #310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
48 changes: 38 additions & 10 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,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.
Expand All @@ -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,
)
Comment on lines +105 to +108
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is used anywhere. It might be possible to remove this entirely and clean the implementation up a bit more.

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
Expand Down
11 changes: 11 additions & 0 deletions test/k4FWCoreTest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <module>]=]
)

add_test_with_env(ParticleIDMetadataFramework options/ExampleParticleIDMetadata.py)


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")