Coverage for pycommons/dev/tests/compile_and_run.py: 100%
40 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-07 11:10 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-07 11:10 +0000
1"""Compile and run some Python code for testing purposes."""
3from os import chdir, getcwd
4from typing import Final
6from pycommons.io.console import logger
7from pycommons.io.path import Path, directory_path
8from pycommons.io.temp import temp_dir
11def compile_and_run(code: str, source: str) -> None:
12 """
13 Compile and run some code for testing purposes.
15 This method first checks the types of its parameters.
16 It then performs some superficial sanity checks on `code`.
17 Then, it changes the working directory to a temporary folder which is
18 deleted after all work is done.
19 It then compiles and, if that was successful, executes the code fragment.
20 Then working directory is changed back to the original directory and the
21 temporary directory is deleted.
23 :param code: the code to be compiled and run
24 :param source: the source of the code
25 :raises TypeError: if `code` or `source` are not strings
26 :raises ValueError: if any parameter has an invalid value
27 or if the code execution fails
29 >>> ed = Path(__file__).up(4).resolve_inside("examples")
30 >>> ef = ed.resolve_inside("compile_and_run_dummy.py")
31 >>> compile_and_run(ef.read_all_str(), ef)
32 compile_and_run_dummy.py
34 >>> wd = getcwd()
35 >>> try:
36 ... compile_and_run(None, "bla")
37 ... except TypeError as te:
38 ... print(te)
39 descriptor '__len__' requires a 'str' object but received a 'NoneType'
41 >>> wd == getcwd()
42 True
44 >>> try:
45 ... compile_and_run(1, "bla")
46 ... except TypeError as te:
47 ... print(te)
48 descriptor '__len__' requires a 'str' object but received a 'int'
50 >>> wd == getcwd()
51 True
53 >>> try:
54 ... compile_and_run("x=5", None)
55 ... except TypeError as te:
56 ... print(te)
57 descriptor '__len__' requires a 'str' object but received a 'NoneType'
59 >>> wd == getcwd()
60 True
62 >>> try:
63 ... compile_and_run("x=5", 1)
64 ... except TypeError as te:
65 ... print(te)
66 descriptor '__len__' requires a 'str' object but received a 'int'
68 >>> wd == getcwd()
69 True
71 >>> try:
72 ... compile_and_run(None, None)
73 ... except TypeError as te:
74 ... print(te)
75 descriptor '__len__' requires a 'str' object but received a 'NoneType'
77 >>> wd == getcwd()
78 True
80 >>> try:
81 ... compile_and_run("x=5", "")
82 ... except ValueError as ve:
83 ... print(ve)
84 Invalid source: ''.
86 >>> wd == getcwd()
87 True
89 >>> try:
90 ... compile_and_run("x=5", " ")
91 ... except ValueError as ve:
92 ... print(ve)
93 Source cannot be only white space, but ' ' is.
95 >>> wd == getcwd()
96 True
98 >>> try:
99 ... compile_and_run("", "x")
100 ... except ValueError as ve:
101 ... print(ve)
102 Code '' from 'x' is empty?
104 >>> wd == getcwd()
105 True
107 >>> try:
108 ... compile_and_run(" ", "x")
109 ... except ValueError as ve:
110 ... print(ve)
111 Code ' ' from 'x' consists of only white space!
113 >>> wd == getcwd()
114 True
116 >>> try:
117 ... compile_and_run("ä ", "x")
118 ... except ValueError as ve:
119 ... print(ve)
120 Code 'ä ' from 'x' contains non-ASCII characters.
122 >>> wd == getcwd()
123 True
125 >>> from contextlib import redirect_stdout
126 >>> try:
127 ... with redirect_stdout(None):
128 ... compile_and_run("<>-sdf/%'!234", "src")
129 ... except ValueError as ve:
130 ... print(ve)
131 Error when compiling 'src'.
133 >>> wd == getcwd()
134 True
136 >>> try:
137 ... with redirect_stdout(None):
138 ... compile_and_run("1/0", "src")
139 ... except ValueError as ve:
140 ... print(ve)
141 Error when executing 'src'.
143 >>> wd == getcwd()
144 True
146 >>> with redirect_stdout(None):
147 ... compile_and_run("print(1)", "src")
149 >>> wd == getcwd()
150 True
151 """
152 if str.__len__(source) <= 0:
153 raise ValueError(f"Invalid source: {source!r}.")
154 use_source: Final[str] = str.strip(source)
155 if str.__len__(use_source) <= 0:
156 raise ValueError(
157 f"Source cannot be only white space, but {source!r} is.")
159 if str.__len__(code) <= 0:
160 raise ValueError(f"Code {code!r} from {source!r} is empty?")
161 use_code: Final[str] = str.rstrip(code)
162 code_len: Final[int] = str.__len__(use_code)
163 if code_len <= 0:
164 raise ValueError(
165 f"Code {code!r} from {source!r} consists of only white space!")
167 if not str.isascii(use_code):
168 raise ValueError(
169 f"Code {code!r} from {source!r} contains non-ASCII characters.")
171 working_dir: Final[str] = directory_path(getcwd())
172 logger(f"Original working directory is {working_dir!r}.")
174 with temp_dir() as td:
175 logger(f"Changing working directory to temp dir {td!r} to "
176 f"process source {use_source!r}.")
177 try:
178 chdir(td)
179 try:
180 compiled = compile( # noqa # nosec
181 use_code, filename=use_source, # noqa # nosec
182 mode="exec", dont_inherit=True) # noqa # nosec
183 except BaseException as be: # noqa
184 raise ValueError(
185 f"Error when compiling {use_source!r}.") from be
186 logger(f"Successfully compiled, now executing {use_source!r}.")
187 args: dict[str, str] = {}
188 try:
189 if isinstance(source, Path) and source.is_file():
190 args["__file__"] = use_source
191 exec(compiled, args) # pylint: disable = W0122 # noqa # nosec
192 except BaseException as be: # noqa
193 raise ValueError(
194 f"Error when executing {use_source!r}.") from be
195 finally:
196 chdir(working_dir)
197 logger(f"Changed working directory back to {working_dir!r}")
198 logger(f"Successfully finished executing code from {use_source!r}.")