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,45 @@ 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
+ lock = filelock .FileLock (generated_path .with_suffix (generated_path .suffix + '.lock' ))
278
+
279
+ with lock :
280
+ if not generated_path .is_file ():
281
+ # if latex or dvipng (dvisvgm) has failed once, don't bother to try again
282
+ latex_failed = hasattr (self .builder , '_imgmath_warned_latex' )
283
+ trans_failed = hasattr (self .builder , '_imgmath_warned_image_translator' )
284
+ if latex_failed or trans_failed :
285
+ return None , None
286
+
287
+ # .tex -> .dvi
288
+ try :
289
+ dvipath = compile_math (latex , self .builder )
290
+ except InvokeError :
291
+ self .builder ._imgmath_warned_latex = True # type: ignore[attr-defined]
292
+ return None , None
293
+
294
+ # .dvi -> .png/.svg
295
+ try :
296
+ if image_format == 'png' :
297
+ depth = convert_dvi_to_png (dvipath , self .builder , generated_path )
298
+ elif image_format == 'svg' :
299
+ depth = convert_dvi_to_svg (dvipath , self .builder , generated_path )
300
+ except InvokeError :
301
+ self .builder ._imgmath_warned_image_translator = True # type: ignore[attr-defined]
302
+ return None , None
303
+
304
+ return generated_path , depth
305
+
306
+ # at this point it has been created
307
+ if image_format == 'png' :
308
+ depth = read_png_depth (generated_path )
309
+ elif image_format == 'svg' :
310
+ depth = read_svg_depth (generated_path )
299
311
return generated_path , depth
300
312
301
313
@@ -319,11 +331,16 @@ def clean_up_files(app: Sphinx, exc: Exception) -> None:
319
331
with contextlib .suppress (Exception ):
320
332
shutil .rmtree (app .builder ._imgmath_tempdir )
321
333
334
+ math_outdir = app .builder .outdir / app .builder .imagedir / 'math'
322
335
if app .builder .config .imgmath_embed :
323
336
# in embed mode, the images are still generated in the math output dir
324
337
# to be shared across workers, but are not useful to the final document
325
338
with contextlib .suppress (Exception ):
326
- shutil .rmtree (app .builder .outdir / app .builder .imagedir / 'math' )
339
+ shutil .rmtree (math_outdir )
340
+ else :
341
+ # cleanup lock files when using parallel workers
342
+ for lockfile in math_outdir .glob ('*.lock' ):
343
+ Path .unlink (lockfile )
327
344
328
345
329
346
def get_tooltip (self : HTML5Translator , node : Element ) -> str :
@@ -383,7 +400,7 @@ def html_visit_displaymath(self: HTML5Translator, node: nodes.math_block) -> Non
383
400
self .body .append ('<p>' )
384
401
if node ['number' ]:
385
402
number = get_node_equation_number (self , node )
386
- self .body .append ('<span class="eqno">(%s)' % number )
403
+ self .body .append (f '<span class="eqno">({ number } )' )
387
404
self .add_permalink_ref (node , _ ('Link to this equation' ))
388
405
self .body .append ('</span>' )
389
406
0 commit comments