17
17
from subprocess import CalledProcessError
18
18
from typing import TYPE_CHECKING
19
19
20
+ import filelock
20
21
from docutils import nodes
21
22
22
23
import sphinx
29
30
from sphinx .util .template import LaTeXRenderer
30
31
31
32
if TYPE_CHECKING :
33
+ from typing import Any
34
+
32
35
from docutils .nodes import Element
33
36
34
37
from sphinx .application import Sphinx
@@ -82,7 +85,7 @@ def read_svg_depth(filename: str | os.PathLike[str]) -> int | None:
82
85
def write_svg_depth (filename : Path , depth : int ) -> None :
83
86
"""Write the depth to SVG file as a comment at end of file"""
84
87
with open (filename , 'a' , encoding = 'utf-8' ) as f :
85
- f .write ('\n <!-- DEPTH=%s -->' % depth )
88
+ f .write (f '\n <!-- DEPTH={ depth } -->' )
86
89
87
90
88
91
def generate_latex_macro (
@@ -266,36 +269,46 @@ def render_math(
266
269
)
267
270
generated_path = self .builder .outdir / self .builder .imagedir / 'math' / filename
268
271
generated_path .parent .mkdir (parents = True , exist_ok = True )
269
- if generated_path .is_file ():
270
- if image_format == 'png' :
271
- depth = read_png_depth (generated_path )
272
- elif image_format == 'svg' :
273
- depth = read_svg_depth (generated_path )
274
- return generated_path , depth
275
-
276
- # if latex or dvipng (dvisvgm) has failed once, don't bother to try again
277
- latex_failed = hasattr (self .builder , '_imgmath_warned_latex' )
278
- trans_failed = hasattr (self .builder , '_imgmath_warned_image_translator' )
279
- if latex_failed or trans_failed :
280
- return None , None
281
-
282
- # .tex -> .dvi
283
- try :
284
- dvipath = compile_math (latex , self .builder )
285
- except InvokeError :
286
- self .builder ._imgmath_warned_latex = True # type: ignore[attr-defined]
287
- return None , None
288
-
289
- # .dvi -> .png/.svg
290
- try :
291
- if image_format == 'png' :
292
- depth = convert_dvi_to_png (dvipath , self .builder , generated_path )
293
- elif image_format == 'svg' :
294
- depth = convert_dvi_to_svg (dvipath , self .builder , generated_path )
295
- except InvokeError :
296
- self .builder ._imgmath_warned_image_translator = True # type: ignore[attr-defined]
297
- return None , None
298
272
273
+ # ensure parallel workers do not try to write the image depth
274
+ # multiple times to achieve reproducible builds
275
+ lock : Any = contextlib .nullcontext ()
276
+ if self .builder .parallel_ok :
277
+ lockfile = generated_path .with_suffix (generated_path .suffix + '.lock' )
278
+ lock = filelock .FileLock (lockfile )
279
+
280
+ with lock :
281
+ if not generated_path .is_file ():
282
+ # if latex or dvipng (dvisvgm) has failed once, don't bother to try again
283
+ latex_failed = hasattr (self .builder , '_imgmath_warned_latex' )
284
+ trans_failed = hasattr (self .builder , '_imgmath_warned_image_translator' )
285
+ if latex_failed or trans_failed :
286
+ return None , None
287
+
288
+ # .tex -> .dvi
289
+ try :
290
+ dvipath = compile_math (latex , self .builder )
291
+ except InvokeError :
292
+ self .builder ._imgmath_warned_latex = True # type: ignore[attr-defined]
293
+ return None , None
294
+
295
+ # .dvi -> .png/.svg
296
+ try :
297
+ if image_format == 'png' :
298
+ depth = convert_dvi_to_png (dvipath , self .builder , generated_path )
299
+ elif image_format == 'svg' :
300
+ depth = convert_dvi_to_svg (dvipath , self .builder , generated_path )
301
+ except InvokeError :
302
+ self .builder ._imgmath_warned_image_translator = True # type: ignore[attr-defined]
303
+ return None , None
304
+
305
+ return generated_path , depth
306
+
307
+ # at this point it has been created
308
+ if image_format == 'png' :
309
+ depth = read_png_depth (generated_path )
310
+ elif image_format == 'svg' :
311
+ depth = read_svg_depth (generated_path )
299
312
return generated_path , depth
300
313
301
314
@@ -319,11 +332,16 @@ def clean_up_files(app: Sphinx, exc: Exception) -> None:
319
332
with contextlib .suppress (Exception ):
320
333
shutil .rmtree (app .builder ._imgmath_tempdir )
321
334
335
+ math_outdir = app .builder .outdir / app .builder .imagedir / 'math'
322
336
if app .builder .config .imgmath_embed :
323
337
# in embed mode, the images are still generated in the math output dir
324
338
# to be shared across workers, but are not useful to the final document
325
339
with contextlib .suppress (Exception ):
326
- shutil .rmtree (app .builder .outdir / app .builder .imagedir / 'math' )
340
+ shutil .rmtree (math_outdir )
341
+ else :
342
+ # cleanup lock files when using parallel workers
343
+ for lockfile in math_outdir .glob ('*.lock' ):
344
+ Path .unlink (lockfile )
327
345
328
346
329
347
def get_tooltip (self : HTML5Translator , node : Element ) -> str :
@@ -383,7 +401,7 @@ def html_visit_displaymath(self: HTML5Translator, node: nodes.math_block) -> Non
383
401
self .body .append ('<p>' )
384
402
if node ['number' ]:
385
403
number = get_node_equation_number (self , node )
386
- self .body .append ('<span class="eqno">(%s)' % number )
404
+ self .body .append (f '<span class="eqno">({ number } )' )
387
405
self .add_permalink_ref (node , _ ('Link to this equation' ))
388
406
self .body .append ('</span>' )
389
407
0 commit comments