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

1"""Compile and run some Python code for testing purposes.""" 

2 

3from os import chdir, getcwd 

4from typing import Final 

5 

6from pycommons.io.console import logger 

7from pycommons.io.path import Path, directory_path 

8from pycommons.io.temp import temp_dir 

9 

10 

11def compile_and_run(code: str, source: str) -> None: 

12 """ 

13 Compile and run some code for testing purposes. 

14 

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. 

22 

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 

28 

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 

33 

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' 

40 

41 >>> wd == getcwd() 

42 True 

43 

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' 

49 

50 >>> wd == getcwd() 

51 True 

52 

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' 

58 

59 >>> wd == getcwd() 

60 True 

61 

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' 

67 

68 >>> wd == getcwd() 

69 True 

70 

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' 

76 

77 >>> wd == getcwd() 

78 True 

79 

80 >>> try: 

81 ... compile_and_run("x=5", "") 

82 ... except ValueError as ve: 

83 ... print(ve) 

84 Invalid source: ''. 

85 

86 >>> wd == getcwd() 

87 True 

88 

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. 

94 

95 >>> wd == getcwd() 

96 True 

97 

98 >>> try: 

99 ... compile_and_run("", "x") 

100 ... except ValueError as ve: 

101 ... print(ve) 

102 Code '' from 'x' is empty? 

103 

104 >>> wd == getcwd() 

105 True 

106 

107 >>> try: 

108 ... compile_and_run(" ", "x") 

109 ... except ValueError as ve: 

110 ... print(ve) 

111 Code ' ' from 'x' consists of only white space! 

112 

113 >>> wd == getcwd() 

114 True 

115 

116 >>> try: 

117 ... compile_and_run("ä ", "x") 

118 ... except ValueError as ve: 

119 ... print(ve) 

120 Code 'ä ' from 'x' contains non-ASCII characters. 

121 

122 >>> wd == getcwd() 

123 True 

124 

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'. 

132 

133 >>> wd == getcwd() 

134 True 

135 

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'. 

142 

143 >>> wd == getcwd() 

144 True 

145 

146 >>> with redirect_stdout(None): 

147 ... compile_and_run("print(1)", "src") 

148 

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

158 

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

166 

167 if not str.isascii(use_code): 

168 raise ValueError( 

169 f"Code {code!r} from {source!r} contains non-ASCII characters.") 

170 

171 working_dir: Final[str] = directory_path(getcwd()) 

172 logger(f"Original working directory is {working_dir!r}.") 

173 

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