Coverage for pycommons / dev / tests / compile_and_run.py: 100%

37 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 03:04 +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 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 >>> wd = getcwd() 

30 >>> try: 

31 ... compile_and_run(None, "bla") 

32 ... except TypeError as te: 

33 ... print(te) 

34 descriptor '__len__' requires a 'str' object but received a 'NoneType' 

35 

36 >>> wd == getcwd() 

37 True 

38 

39 >>> try: 

40 ... compile_and_run(1, "bla") 

41 ... except TypeError as te: 

42 ... print(te) 

43 descriptor '__len__' requires a 'str' object but received a 'int' 

44 

45 >>> wd == getcwd() 

46 True 

47 

48 >>> try: 

49 ... compile_and_run("x=5", None) 

50 ... except TypeError as te: 

51 ... print(te) 

52 descriptor '__len__' requires a 'str' object but received a 'NoneType' 

53 

54 >>> wd == getcwd() 

55 True 

56 

57 >>> try: 

58 ... compile_and_run("x=5", 1) 

59 ... except TypeError as te: 

60 ... print(te) 

61 descriptor '__len__' requires a 'str' object but received a 'int' 

62 

63 >>> wd == getcwd() 

64 True 

65 

66 >>> try: 

67 ... compile_and_run(None, None) 

68 ... except TypeError as te: 

69 ... print(te) 

70 descriptor '__len__' requires a 'str' object but received a 'NoneType' 

71 

72 >>> wd == getcwd() 

73 True 

74 

75 >>> try: 

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

77 ... except ValueError as ve: 

78 ... print(ve) 

79 Invalid source: ''. 

80 

81 >>> wd == getcwd() 

82 True 

83 

84 >>> try: 

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

86 ... except ValueError as ve: 

87 ... print(ve) 

88 Source cannot be only white space, but ' ' is. 

89 

90 >>> wd == getcwd() 

91 True 

92 

93 >>> try: 

94 ... compile_and_run("", "x") 

95 ... except ValueError as ve: 

96 ... print(ve) 

97 Code '' from 'x' is empty? 

98 

99 >>> wd == getcwd() 

100 True 

101 

102 >>> try: 

103 ... compile_and_run(" ", "x") 

104 ... except ValueError as ve: 

105 ... print(ve) 

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

107 

108 >>> wd == getcwd() 

109 True 

110 

111 >>> try: 

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

113 ... except ValueError as ve: 

114 ... print(ve) 

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

116 

117 >>> wd == getcwd() 

118 True 

119 

120 >>> from contextlib import redirect_stdout 

121 >>> try: 

122 ... with redirect_stdout(None): 

123 ... compile_and_run("<>-sdf/%'!234", "src") 

124 ... except ValueError as ve: 

125 ... print(ve) 

126 Error when compiling 'src'. 

127 

128 >>> wd == getcwd() 

129 True 

130 

131 >>> try: 

132 ... with redirect_stdout(None): 

133 ... compile_and_run("1/0", "src") 

134 ... except ValueError as ve: 

135 ... print(ve) 

136 Error when executing 'src'. 

137 

138 >>> wd == getcwd() 

139 True 

140 

141 >>> with redirect_stdout(None): 

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

143 

144 >>> wd == getcwd() 

145 True 

146 """ 

147 if str.__len__(source) <= 0: 

148 raise ValueError(f"Invalid source: {source!r}.") 

149 use_source: Final[str] = str.strip(source) 

150 if str.__len__(use_source) <= 0: 

151 raise ValueError( 

152 f"Source cannot be only white space, but {source!r} is.") 

153 

154 if str.__len__(code) <= 0: 

155 raise ValueError(f"Code {code!r} from {source!r} is empty?") 

156 use_code: Final[str] = str.rstrip(code) 

157 code_len: Final[int] = str.__len__(use_code) 

158 if code_len <= 0: 

159 raise ValueError( 

160 f"Code {code!r} from {source!r} consists of only white space!") 

161 

162 if not str.isascii(use_code): 

163 raise ValueError( 

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

165 

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

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

168 

169 with temp_dir() as td: 

170 logger(f"Changing working directory to temp dir {td!r} to " 

171 f"process source {use_source!r}.") 

172 try: 

173 chdir(td) 

174 try: 

175 compiled = compile( # noqa # nosec 

176 use_code, filename=use_source, # noqa # nosec 

177 mode="exec", dont_inherit=True) # noqa # nosec 

178 except BaseException as be: # noqa 

179 raise ValueError( 

180 f"Error when compiling {use_source!r}.") from be 

181 logger(f"Successfully compiled, now executing {use_source!r}.") 

182 try: 

183 exec(compiled, {}) # pylint: disable = W0122 # noqa # nosec 

184 except BaseException as be: # noqa 

185 raise ValueError( 

186 f"Error when executing {use_source!r}.") from be 

187 finally: 

188 chdir(working_dir) 

189 logger(f"Changed working directory back to {working_dir!r}") 

190 logger(f"Successfully finished executing code from {use_source!r}.")