Coverage for ase / utils / sphinx_create_png.py: 12.58%

159 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-21 15:52 +0000

1import os 

2import re 

3import runpy 

4import traceback 

5import warnings 

6from os.path import join 

7from pathlib import Path 

8from stat import ST_MTIME 

9from subprocess import DEVNULL, CalledProcessError, check_call 

10 

11from docutils import nodes 

12from docutils.parsers.rst.roles import set_classes 

13 

14 

15def png_scraper(block, block_vars, gallery_conf): 

16 import shutil 

17 

18 from sphinx_gallery.scrapers import figure_rst 

19 

20 src_file = Path(block_vars['src_file']) 

21 src_dir = src_file.parent 

22 

23 pngs = sorted(src_dir.glob('*.png')) 

24 

25 image_names = [] 

26 image_path_iterator = block_vars['image_path_iterator'] 

27 

28 for png in pngs: 

29 this_image_path = image_path_iterator.next() 

30 image_names.append(this_image_path) 

31 shutil.move(png, this_image_path) 

32 

33 return figure_rst(image_names, gallery_conf['src_dir']) 

34 

35 

36def mol_role(role, rawtext, text, lineno, inliner, options={}, content=[]): 

37 n = [] 

38 t = '' 

39 while text: 

40 if text[0] == '_': 

41 n.append(nodes.inline(text=t)) 

42 t = '' 

43 m = re.match(r'\d+', text[1:]) 

44 if m is None: 

45 raise RuntimeError('Expected one or more digits after "_"') 

46 digits = m.group() 

47 n.append(nodes.subscript(text=digits)) 

48 text = text[1 + len(digits) :] 

49 else: 

50 t += text[0] 

51 text = text[1:] 

52 n.append(nodes.inline(text=t)) 

53 return n, [] 

54 

55 

56def git_role_tmpl( 

57 urlroot, role, rawtext, text, lineno, inliner, options={}, content=[] 

58): 

59 env = inliner.document.settings.env 

60 srcdir = Path(env.srcdir) 

61 project_root = srcdir.parent 

62 # The asserts below are commented out because this role template 

63 # is also used by other projects, such as GPAW and Asap. 

64 # assert srcdir.name == 'doc' 

65 # assert project_root.name == 'ase' 

66 

67 if text[-1] == '>': 

68 i = text.index('<') 

69 name = text[: i - 1] 

70 text = text[i + 1 : -1] 

71 else: 

72 name = text 

73 if name[0] == '~': 

74 name = name.split('/')[-1] 

75 text = text[1:] 

76 if '?' in name: 

77 name = name[: name.index('?')] 

78 

79 # Check if the link is broken 

80 is_tag = text.startswith('..') # Tags are like :git:`3.19.1 <../3.19.1>` 

81 path = project_root / text 

82 

83 if not (is_tag or path.exists()): 

84 msg = f'Broken link: {rawtext}: Non-existing path: {path}' 

85 msg = inliner.reporter.error(msg, line=lineno) 

86 prb = inliner.problematic(rawtext, rawtext, msg) 

87 return [prb], [msg] 

88 ref = urlroot + text 

89 set_classes(options) 

90 node = nodes.reference(rawtext, name, refuri=ref, **options) 

91 return [node], [] 

92 

93 

94def git_role(role, rawtext, text, lineno, inliner, options={}, content=[]): 

95 return git_role_tmpl( 

96 'https://gitlab.com/ase/ase/blob/master/', 

97 role, 

98 rawtext, 

99 text, 

100 lineno, 

101 inliner, 

102 options, 

103 content, 

104 ) 

105 

106 

107def setup(app): 

108 app.add_role('mol', mol_role) 

109 app.add_role('git', git_role) 

110 create_png_files() 

111 

112 

113def creates(): 

114 """Generator for Python scripts and their output filenames.""" 

115 for dirpath, dirnames, filenames in sorted(os.walk('.')): 

116 if dirpath.startswith('./build'): 

117 # Skip files in the build/ folder 

118 continue 

119 

120 for filename in sorted(filenames): 

121 if filename.endswith('.py'): 

122 path = join(dirpath, filename) 

123 with open(path) as fd: 

124 lines = fd.readlines() 

125 if len(lines) == 0: 

126 continue 

127 if 'coding: utf-8' in lines[0]: 

128 lines.pop(0) 

129 outnames = [] 

130 for line in lines: 

131 if line.startswith('# creates:'): 

132 outnames.extend( 

133 [file.rstrip(',') for file in line.split()[2:]] 

134 ) 

135 else: 

136 break 

137 if outnames: 

138 yield dirpath, filename, outnames 

139 

140 

141def create_png_files(raise_exceptions=False): 

142 from ase.utils import workdir 

143 

144 try: 

145 check_call(['povray', '-h'], stderr=DEVNULL) 

146 except (FileNotFoundError, CalledProcessError): 

147 warnings.warn('No POVRAY!') 

148 # Replace write_pov with write_png: 

149 from ase.io import pov 

150 from ase.io.png import write_png 

151 

152 def write_pov( 

153 filename, 

154 atoms, 

155 povray_settings={}, 

156 isosurface_data=None, 

157 **generic_projection_settings, 

158 ): 

159 write_png( 

160 Path(filename).with_suffix('.png'), 

161 atoms, 

162 **generic_projection_settings, 

163 ) 

164 

165 class DummyRenderer: 

166 def render(self): 

167 pass 

168 

169 return DummyRenderer() 

170 

171 pov.write_pov = write_pov 

172 

173 for dir, pyname, outnames in creates(): 

174 path = join(dir, pyname) 

175 t0 = os.stat(path)[ST_MTIME] 

176 run = False 

177 for outname in outnames: 

178 try: 

179 t = os.stat(join(dir, outname))[ST_MTIME] 

180 except OSError: 

181 run = True 

182 break 

183 else: 

184 if t < t0: 

185 run = True 

186 break 

187 if run: 

188 print('running:', path) 

189 with workdir(dir): 

190 import matplotlib.pyplot as plt 

191 

192 plt.figure() 

193 try: 

194 runpy.run_path(pyname) 

195 except KeyboardInterrupt: 

196 return 

197 except Exception: 

198 if raise_exceptions: 

199 raise 

200 else: 

201 traceback.print_exc() 

202 

203 for n in plt.get_fignums(): 

204 plt.close(n) 

205 

206 for outname in outnames: 

207 print(dir, outname) 

208 

209 

210def clean(): 

211 """Remove all generated files.""" 

212 for dir, pyname, outnames in creates(): 

213 for outname in outnames: 

214 if os.path.isfile(os.path.join(dir, outname)): 

215 os.remove(os.path.join(dir, outname)) 

216 

217 

218def visual_inspection(): 

219 """Manually inspect generated files.""" 

220 import subprocess 

221 

222 images = [] 

223 text = [] 

224 pdf = [] 

225 for dir, pyname, outnames in creates(): 

226 for outname in outnames: 

227 path = os.path.join(dir, outname) 

228 ext = path.rsplit('.', 1)[1] 

229 if ext == 'pdf': 

230 pdf.append(path) 

231 elif ext in ['csv', 'txt', 'out', 'css', 'LDA', 'rst']: 

232 text.append(path) 

233 else: 

234 images.append(path) 

235 subprocess.call(['eog'] + images) 

236 subprocess.call(['evince'] + pdf) 

237 subprocess.call(['more'] + text) 

238 

239 

240if __name__ == '__main__': 

241 import argparse 

242 

243 parser = argparse.ArgumentParser(description='Process generated files.') 

244 parser.add_argument( 

245 'command', 

246 nargs='?', 

247 default='list', 

248 choices=['list', 'inspect', 'clean', 'run'], 

249 ) 

250 args = parser.parse_args() 

251 if args.command == 'clean': 

252 clean() 

253 elif args.command == 'list': 

254 for dir, pyname, outnames in creates(): 

255 for outname in outnames: 

256 print(os.path.join(dir, outname)) 

257 elif args.command == 'run': 

258 create_png_files(raise_exceptions=True) 

259 else: 

260 visual_inspection()