icplot 0.3.3__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {icplot-0.3.3/src/icplot.egg-info → icplot-0.4.0}/PKG-INFO +41 -9
  2. {icplot-0.3.3 → icplot-0.4.0}/README.md +19 -2
  3. {icplot-0.3.3 → icplot-0.4.0}/pyproject.toml +48 -14
  4. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/converter/image.py +6 -1
  5. icplot-0.4.0/src/icplot/renderer/config.py +73 -0
  6. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/formats/tex.py +3 -1
  7. icplot-0.4.0/src/icplot/renderer/frontmatter.py +62 -0
  8. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/graph/matplotlib.py +2 -2
  9. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/render.py +86 -44
  10. {icplot-0.3.3 → icplot-0.4.0/src/icplot.egg-info}/PKG-INFO +41 -9
  11. {icplot-0.3.3 → icplot-0.4.0}/src/icplot.egg-info/SOURCES.txt +2 -0
  12. {icplot-0.3.3 → icplot-0.4.0}/src/icplot.egg-info/requires.txt +27 -5
  13. icplot-0.4.0/test/test_frontmatter.py +52 -0
  14. icplot-0.4.0/test/test_image.py +37 -0
  15. icplot-0.3.3/src/icplot/renderer/config.py +0 -41
  16. icplot-0.3.3/test/test_image.py +0 -21
  17. {icplot-0.3.3 → icplot-0.4.0}/LICENSE +0 -0
  18. {icplot-0.3.3 → icplot-0.4.0}/setup.cfg +0 -0
  19. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/__init__.py +0 -0
  20. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/charts/__init__.py +0 -0
  21. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/charts/gantt.py +0 -0
  22. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/charts/tree.py +0 -0
  23. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/converter/__init__.py +0 -0
  24. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/converter/cli.py +0 -0
  25. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/converter/convert.py +0 -0
  26. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/__init__.py +0 -0
  27. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/base.py +0 -0
  28. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/__init__.py +0 -0
  29. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/cell.py +0 -0
  30. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/edge.py +0 -0
  31. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/mesh.py +0 -0
  32. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/openfoam/__init__.py +0 -0
  33. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/openfoam/blockmesh.py +0 -0
  34. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/openfoam/foamfile.py +0 -0
  35. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/openfoam/polymesh.py +0 -0
  36. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/operations.py +0 -0
  37. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/shapes.py +0 -0
  38. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/vertex.py +0 -0
  39. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/mesh/vtk.py +0 -0
  40. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/operations.py +0 -0
  41. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/shapes.py +0 -0
  42. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/geometry/transform.py +0 -0
  43. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/graph/__init__.py +0 -0
  44. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/graph/axis.py +0 -0
  45. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/graph/plot.py +0 -0
  46. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/graph/plot_group.py +0 -0
  47. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/graph/series.py +0 -0
  48. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/main_cli.py +0 -0
  49. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/py.typed +0 -0
  50. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/__init__.py +0 -0
  51. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/charts/pgfgantt.py +0 -0
  52. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/charts/svggantt.py +0 -0
  53. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/cli.py +0 -0
  54. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/formats/__init__.py +0 -0
  55. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/formats/mermaid.py +0 -0
  56. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/formats/plantuml.py +0 -0
  57. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/graph/__init__.py +0 -0
  58. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/graph/color.py +0 -0
  59. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/graph/mpl.py +0 -0
  60. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/graph/renderer.py +0 -0
  61. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/graph/vtk.py +0 -0
  62. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/scene/cairo.py +0 -0
  63. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/renderer/video.py +0 -0
  64. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/scene.py +0 -0
  65. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/theme/__init__.py +0 -0
  66. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/theme/cli.py +0 -0
  67. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/theme/color.py +0 -0
  68. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/theme/templates/puml.j2 +0 -0
  69. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/theme/templates/tikz.j2 +0 -0
  70. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/theme/theme.json +0 -0
  71. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/theme/theme.py +0 -0
  72. {icplot-0.3.3 → icplot-0.4.0}/src/icplot/theme/theme_generator.py +0 -0
  73. {icplot-0.3.3 → icplot-0.4.0}/src/icplot.egg-info/dependency_links.txt +0 -0
  74. {icplot-0.3.3 → icplot-0.4.0}/src/icplot.egg-info/entry_points.txt +0 -0
  75. {icplot-0.3.3 → icplot-0.4.0}/src/icplot.egg-info/top_level.txt +0 -0
  76. {icplot-0.3.3 → icplot-0.4.0}/test/test_cairo.py +0 -0
  77. {icplot-0.3.3 → icplot-0.4.0}/test/test_gantt_chart.py +0 -0
  78. {icplot-0.3.3 → icplot-0.4.0}/test/test_geometry.py +0 -0
  79. {icplot-0.3.3 → icplot-0.4.0}/test/test_line_plots.py +0 -0
  80. {icplot-0.3.3 → icplot-0.4.0}/test/test_mermaid.py +0 -0
  81. {icplot-0.3.3 → icplot-0.4.0}/test/test_meshing.py +0 -0
  82. {icplot-0.3.3 → icplot-0.4.0}/test/test_openfoam.py +0 -0
  83. {icplot-0.3.3 → icplot-0.4.0}/test/test_plot_group.py +0 -0
  84. {icplot-0.3.3 → icplot-0.4.0}/test/test_polymesh.py +0 -0
  85. {icplot-0.3.3 → icplot-0.4.0}/test/test_puml.py +0 -0
  86. {icplot-0.3.3 → icplot-0.4.0}/test/test_tex.py +0 -0
  87. {icplot-0.3.3 → icplot-0.4.0}/test/test_tree.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: icplot
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Utilities for generating plots and graphics for technical reports.
5
5
  Author-email: "James Grogan, Irish Centre for High End Computing" <james.grogan@ichec.ie>
6
6
  Project-URL: Repository, https://git.ichec.ie/performance/toolshed/icplot
@@ -14,11 +14,6 @@ Classifier: Topic :: System :: Distributed Computing
14
14
  Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: wand
18
- Requires-Dist: matplotlib
19
- Requires-Dist: vtk
20
- Requires-Dist: moviepy
21
- Requires-Dist: scipy
22
17
  Requires-Dist: iccore==0.2.3
23
18
  Requires-Dist: graphviz
24
19
  Requires-Dist: Jinja2
@@ -31,9 +26,29 @@ Requires-Dist: isort; extra == "test"
31
26
  Requires-Dist: mypy; extra == "test"
32
27
  Requires-Dist: flake8; extra == "test"
33
28
  Requires-Dist: pylint; extra == "test"
29
+ Requires-Dist: types-PyYAML; extra == "test"
34
30
  Provides-Extra: cairo
35
- Requires-Dist: pycairo; extra == "cairo"
36
31
  Requires-Dist: CairoSVG; extra == "cairo"
32
+ Provides-Extra: scene
33
+ Requires-Dist: pycairo; extra == "scene"
34
+ Provides-Extra: imagemagick
35
+ Requires-Dist: wand; extra == "imagemagick"
36
+ Provides-Extra: plot
37
+ Requires-Dist: matplotlib; extra == "plot"
38
+ Provides-Extra: vtk
39
+ Requires-Dist: vtk; extra == "vtk"
40
+ Provides-Extra: geometry
41
+ Requires-Dist: scipy; extra == "geometry"
42
+ Provides-Extra: video
43
+ Requires-Dist: moviepy; extra == "video"
44
+ Provides-Extra: all
45
+ Requires-Dist: CairoSVG; extra == "all"
46
+ Requires-Dist: pycairo; extra == "all"
47
+ Requires-Dist: wand; extra == "all"
48
+ Requires-Dist: matplotlib; extra == "all"
49
+ Requires-Dist: vtk; extra == "all"
50
+ Requires-Dist: scipy; extra == "all"
51
+ Requires-Dist: moviepy; extra == "all"
37
52
  Dynamic: license-file
38
53
 
39
54
  # icplot
@@ -121,13 +136,30 @@ The library supports themeing with an Angular Material 3 motivated design system
121
136
 
122
137
  # Installation #
123
138
 
124
- The package is available on PyPI. For a minimal installation you can do:
139
+ The package is available on PyPI. The core install is deliberately light — it
140
+ does not pull the heavy rendering backends, so consumers that only need part of
141
+ the library (e.g. a docs build that just converts svg to png) stay small:
125
142
 
126
143
  ``` shell
127
144
  pip install icplot
128
145
  ```
129
146
 
130
- For full functionality, particularly conversion of image formats, `imagemagick` and `cairo` are required. On Mac you can install them with:
147
+ Backends are opt-in via extras:
148
+
149
+ | Extra | Pulls | Enables |
150
+ |---|---|---|
151
+ | `cairo` | CairoSVG | svg → png / pdf conversion |
152
+ | `scene` | pycairo | vector scene drawing (`renderer.scene.cairo`) |
153
+ | `imagemagick` | wand | pdf → png conversion (falls back to the `magick` CLI) |
154
+ | `plot` | matplotlib | 2D plotting (`renderer.graph`, `charts`) |
155
+ | `vtk` | vtk | 3D / mesh rendering (`geometry.mesh`) |
156
+ | `geometry` | scipy | geometry transforms (`geometry.operations`) |
157
+ | `video` | moviepy | video assembly (`renderer.video`) |
158
+ | `all` | all of the above | everything (previous default behaviour) |
159
+
160
+ For example, `pip install icplot[cairo]` for svg conversion, or
161
+ `pip install icplot[all]` for the full toolkit. The native `cairo` and
162
+ `imagemagick` system libraries are also required for those backends; on Mac:
131
163
 
132
164
  ``` shell
133
165
  brew install imagemagick cairo
@@ -83,13 +83,30 @@ The library supports themeing with an Angular Material 3 motivated design system
83
83
 
84
84
  # Installation #
85
85
 
86
- The package is available on PyPI. For a minimal installation you can do:
86
+ The package is available on PyPI. The core install is deliberately light — it
87
+ does not pull the heavy rendering backends, so consumers that only need part of
88
+ the library (e.g. a docs build that just converts svg to png) stay small:
87
89
 
88
90
  ``` shell
89
91
  pip install icplot
90
92
  ```
91
93
 
92
- For full functionality, particularly conversion of image formats, `imagemagick` and `cairo` are required. On Mac you can install them with:
94
+ Backends are opt-in via extras:
95
+
96
+ | Extra | Pulls | Enables |
97
+ |---|---|---|
98
+ | `cairo` | CairoSVG | svg → png / pdf conversion |
99
+ | `scene` | pycairo | vector scene drawing (`renderer.scene.cairo`) |
100
+ | `imagemagick` | wand | pdf → png conversion (falls back to the `magick` CLI) |
101
+ | `plot` | matplotlib | 2D plotting (`renderer.graph`, `charts`) |
102
+ | `vtk` | vtk | 3D / mesh rendering (`geometry.mesh`) |
103
+ | `geometry` | scipy | geometry transforms (`geometry.operations`) |
104
+ | `video` | moviepy | video assembly (`renderer.video`) |
105
+ | `all` | all of the above | everything (previous default behaviour) |
106
+
107
+ For example, `pip install icplot[cairo]` for svg conversion, or
108
+ `pip install icplot[all]` for the full toolkit. The native `cairo` and
109
+ `imagemagick` system libraries are also required for those backends; on Mac:
93
110
 
94
111
  ``` shell
95
112
  brew install imagemagick cairo
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "icplot"
3
- version = "0.3.3"
3
+ version = "0.4.0"
4
4
  authors = [
5
5
  { name="James Grogan, Irish Centre for High End Computing", email="james.grogan@ichec.ie" },
6
6
  ]
@@ -16,14 +16,14 @@ classifiers = [
16
16
  ]
17
17
  keywords = ["Publishing", "Technical Reports", "Graphics"]
18
18
 
19
- dependencies = ["wand",
20
- "matplotlib",
21
- "vtk",
22
- "moviepy",
23
- "scipy",
24
- "iccore==0.2.3",
25
- "graphviz",
26
- "Jinja2"]
19
+ # Core is deliberately light: only what every import path needs. The heavy
20
+ # rendering backends (matplotlib/vtk/scipy/moviepy/wand) are optional extras
21
+ # so consumers that only use the converter or diagram renderer (e.g. the
22
+ # handbook build) can install a small footprint. Use icplot[all] for the
23
+ # previous behaviour of pulling every backend.
24
+ dependencies = ["iccore==0.2.3",
25
+ "graphviz",
26
+ "Jinja2"]
27
27
 
28
28
  [project.urls]
29
29
  Repository = "https://git.ichec.ie/performance/toolshed/icplot"
@@ -38,14 +38,47 @@ test = [
38
38
  "isort",
39
39
  "mypy",
40
40
  "flake8",
41
- "pylint"
41
+ "pylint",
42
+ "types-PyYAML"
42
43
  ]
43
44
 
45
+ # svg/pdf conversion (converter.svg_to_png / svg_to_pdf). CairoSVG pulls
46
+ # cairocffi, which dlopens libcairo at runtime (no C build step needed).
44
47
  cairo = [
45
- "pycairo",
46
48
  "CairoSVG"
47
49
  ]
48
50
 
51
+ # vector scene drawing (renderer.scene.cairo). pycairo (module `cairo`)
52
+ # builds against libcairo — needs the cairo headers + a C toolchain at install.
53
+ scene = ["pycairo"]
54
+
55
+ # raster conversion via ImageMagick bindings (converter.pdf_to_png);
56
+ # falls back to the `magick` CLI if wand is absent
57
+ imagemagick = ["wand"]
58
+
59
+ # 2D plotting backend (icplot.renderer.graph / icplot.charts)
60
+ plot = ["matplotlib"]
61
+
62
+ # 3D / mesh backend (icplot.geometry.mesh, vtk graph backend)
63
+ vtk = ["vtk"]
64
+
65
+ # geometry transforms (icplot.geometry.operations)
66
+ geometry = ["scipy"]
67
+
68
+ # video assembly (icplot.renderer.video)
69
+ video = ["moviepy"]
70
+
71
+ # convenience aggregate — every backend (pre-split behaviour)
72
+ all = [
73
+ "CairoSVG",
74
+ "pycairo",
75
+ "wand",
76
+ "matplotlib",
77
+ "vtk",
78
+ "scipy",
79
+ "moviepy"
80
+ ]
81
+
49
82
  [build-system]
50
83
  requires = ["setuptools", "wheel"]
51
84
  build-backend = "setuptools.build_meta"
@@ -113,12 +146,13 @@ addopts = "--cov=icplot --cov-report term --cov-fail-under=55 --cov-report xml:c
113
146
 
114
147
  [tool.tox]
115
148
  requires = ["tox"]
116
- env_list = ["format_check", "lint", "style", "type", "3.10", "3.13", "docs"]
149
+ env_list = ["format_check", "lint", "style", "type", "3.10", "3.14", "docs"]
117
150
  skip_missing_interpreters = true
118
151
 
119
152
  [tool.tox.env_run_base]
120
153
  description = "Run Unit and Integration Tests"
121
- deps = [".[test, cairo]"]
154
+ # `all` so the full suite (matplotlib/vtk/scipy/moviepy backends) still runs
155
+ deps = [".[test, all]"]
122
156
  commands = [["pytest", "test"]]
123
157
 
124
158
  [tool.tox.env.format_apply]
@@ -153,7 +187,7 @@ commands = [["flake8", "src"]]
153
187
 
154
188
  [tool.tox.env.type]
155
189
  description = "Run Type Checks"
156
- deps = ["mypy", ".[types]"]
190
+ deps = ["mypy", ".[test]"]
157
191
  commands = [["mypy", "src"]]
158
192
 
159
193
  [tool.tox.env.docs]
@@ -45,12 +45,17 @@ def _get_out_filename(source: Path, target: Path | None, extension: str) -> Path
45
45
  return source.parent / f"{source.stem}.{extension}"
46
46
 
47
47
 
48
+ # The order of ImageMagick arguments was changed so that `-alpha off`
49
+ # is applied after loading the source file rather than before.
50
+ # This ensures proper handling when converting PDFs/SVGs,
51
+ # as some versions of ImageMagick ignore or misapply `-alpha off` if it
52
+ # appears before the input file.
48
53
  def _pdf_to_png(source: Path, target: Path | None = None, resolution: int = 300):
49
54
 
50
55
  outfile = _get_out_filename(source, target, "png")
51
56
  os.makedirs(outfile.parent, exist_ok=True)
52
57
  cmd = (
53
- f"magick -density {resolution} -background white -alpha off {source} {outfile}"
58
+ f"magick -density {resolution} -background white {source} -alpha off {outfile}"
54
59
  )
55
60
  subprocess.run(cmd, shell=True, check=True)
56
61
 
@@ -0,0 +1,73 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from pydantic import BaseModel
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class TexConfig(BaseModel, frozen=True):
10
+ """
11
+ This holds settings for the Tex build
12
+ """
13
+
14
+ build_engine: str = "pdflatex"
15
+ nonstopmode: bool = True
16
+ use_woff_fonts: bool = True
17
+ templates: list[Path] = []
18
+
19
+
20
+ class PlantUmlConfig(BaseModel, frozen=True):
21
+ config_path: Path | None = None
22
+
23
+
24
+ class MermaidConfig(BaseModel, frozen=True):
25
+ browser_config: Path | None = None
26
+
27
+
28
+ class RenderConfig(BaseModel, frozen=True):
29
+ tex: TexConfig = TexConfig()
30
+ plantuml: PlantUmlConfig = PlantUmlConfig()
31
+ mermaid: MermaidConfig = MermaidConfig()
32
+ extra_config: Path | None = None
33
+ input_formats: list[str] = []
34
+ output_formats: list[str] = []
35
+ excludes: list[str] = []
36
+
37
+
38
+ def apply_frontmatter(config: "RenderConfig", frontmatter: dict) -> "RenderConfig":
39
+ """
40
+ Return a copy of *config* with per-file overrides from *frontmatter* applied.
41
+
42
+ Recognised keys
43
+ ---------------
44
+ theme : str
45
+ ``"none"`` — suppress the global plantuml config_path for this file.
46
+ Any other value (or absent) — leave config_path unchanged.
47
+ meta : dict
48
+ Stored for future use (SVG metadata injection). Currently a no-op.
49
+ """
50
+ if not frontmatter:
51
+ return config
52
+
53
+ updated_plantuml = config.plantuml
54
+
55
+ theme = frontmatter.get("theme")
56
+ if theme == "none":
57
+ updated_plantuml = config.plantuml.model_copy(update={"config_path": None})
58
+ elif theme not in (None, "platform"):
59
+ logger.warning("Unknown frontmatter theme value %r — ignoring", theme)
60
+
61
+ if "meta" in frontmatter:
62
+ logger.debug("Frontmatter meta present — SVG injection not yet implemented")
63
+
64
+ return config.model_copy(update={"plantuml": updated_plantuml})
65
+
66
+
67
+ class RenderContext(BaseModel, frozen=True):
68
+ config: RenderConfig
69
+ source: Path
70
+ output_type: str
71
+ build_dir: Path
72
+ output_dir: Path | None = None
73
+ clear_workdir: bool = True
@@ -2,6 +2,7 @@
2
2
  This module has functionality for publishing Tex documents
3
3
  """
4
4
 
5
+ import hashlib
5
6
  import logging
6
7
  import os
7
8
  import shutil
@@ -90,7 +91,8 @@ def render(ctx: RenderContext):
90
91
  ctx.config.tex.build_engine,
91
92
  )
92
93
 
93
- work_dir = ctx.build_dir / ctx.source.stem
94
+ path_hash = hashlib.md5(str(ctx.source).encode()).hexdigest()[:8]
95
+ work_dir = ctx.build_dir / f"{ctx.source.stem}_{path_hash}"
94
96
  work_dir.mkdir(exist_ok=True, parents=True)
95
97
 
96
98
  err = _run_commands(ctx, work_dir)
@@ -0,0 +1,62 @@
1
+ """
2
+ Parse icplot frontmatter from diagram source files.
3
+
4
+ Frontmatter is embedded in a PlantUML block comment immediately after @startuml:
5
+
6
+ @startuml
7
+ /' icplot
8
+ theme: none
9
+ meta:
10
+ description: My diagram
11
+ '/
12
+
13
+ The block is valid PlantUML (a block comment) and is ignored by the renderer.
14
+ icplot reads it before invoking the renderer to control per-file behaviour.
15
+ """
16
+
17
+ import logging
18
+ import re
19
+ from pathlib import Path
20
+
21
+ import yaml
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ _OPEN = re.compile(r"^\s*/'[ \t]*icplot[ \t]*$")
26
+ _CLOSE = re.compile(r"^\s*'/\s*$")
27
+
28
+
29
+ def parse(path: Path) -> dict:
30
+ """
31
+ Return the icplot frontmatter from *path* as a dict.
32
+
33
+ Returns an empty dict if no frontmatter block is present or if the block
34
+ cannot be parsed as YAML.
35
+ """
36
+ try:
37
+ lines = path.read_text(encoding="utf-8").splitlines()
38
+ except OSError as exc:
39
+ logger.warning("Could not read %s for frontmatter: %s", path, exc)
40
+ return {}
41
+
42
+ in_block = False
43
+ block_lines: list[str] = []
44
+
45
+ for line in lines:
46
+ if not in_block:
47
+ if _OPEN.match(line):
48
+ in_block = True
49
+ else:
50
+ if _CLOSE.match(line):
51
+ break
52
+ block_lines.append(line)
53
+
54
+ if not block_lines:
55
+ return {}
56
+
57
+ try:
58
+ result = yaml.safe_load("\n".join(block_lines))
59
+ return result if isinstance(result, dict) else {}
60
+ except yaml.YAMLError as exc:
61
+ logger.warning("Could not parse icplot frontmatter in %s: %s", path, exc)
62
+ return {}
@@ -2,9 +2,9 @@ import logging
2
2
  from pathlib import Path
3
3
  from typing import cast
4
4
 
5
- from icplot.graph.series import ( # NOQA
5
+ from icplot.graph.series import LinePlotSeries # NOQA
6
+ from icplot.graph.series import (
6
7
  ImageSeries,
7
- LinePlotSeries,
8
8
  PlotSeries,
9
9
  ScatterPlotSeries,
10
10
  )
@@ -5,42 +5,72 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
5
5
  from functools import partial
6
6
  from pathlib import Path
7
7
 
8
- from .config import RenderConfig, RenderContext
8
+ from .config import RenderConfig, RenderContext, apply_frontmatter
9
9
  from .formats.mermaid import is_supported as mmd_supported
10
10
  from .formats.mermaid import render as render_mmd
11
11
  from .formats.plantuml import is_supported as puml_supported
12
12
  from .formats.plantuml import render as render_puml
13
13
  from .formats.tex import is_supported as tex_supported
14
14
  from .formats.tex import render as render_tex
15
+ from .frontmatter import parse as parse_frontmatter
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
18
-
19
19
  _ENVIRONMENT_CHECKS = {
20
20
  "tex": tex_supported,
21
21
  "puml": puml_supported,
22
22
  "mmd": mmd_supported,
23
23
  }
24
24
 
25
- _RENDERERS = {"tex": render_tex, "puml": render_puml, "mmd": render_mmd}
25
+ _RENDERERS = {
26
+ "tex": render_tex,
27
+ "puml": render_puml,
28
+ "mmd": render_mmd,
29
+ }
26
30
 
27
31
  _FORMATS = ["puml", "mmd", "tex"]
28
32
 
29
- _DEFAULT_OUTPUT_FORMATS = {"tex": ["svg"], "puml": ["png", "svg"], "mmd": ["svg"]}
33
+ _DEFAULT_OUTPUT_FORMATS = {
34
+ "tex": ["svg"],
35
+ "puml": ["png", "svg"],
36
+ "mmd": ["svg"],
37
+ }
38
+
39
+
40
+ def _fail_task(ctx: RenderContext, reason: str):
41
+ """Return failure reason"""
42
+ logger.debug("Fail task triggered for %s: %s", ctx.source, reason)
43
+ return reason
30
44
 
31
45
 
32
46
  def run_tasks(tasks: list) -> list:
47
+ """
48
+ Execute render tasks and collect failures.
49
+ Returns a list of dicts: {"source": Path, "error": str}
50
+ """
33
51
  errors = []
52
+
34
53
  with ThreadPoolExecutor() as executor:
35
54
  futures = {}
36
55
  for t in tasks:
37
56
  ctx = t.args[0]
38
57
  logger.info("Queuing: %s -> %s", ctx.source.name, ctx.output_type)
39
58
  futures[executor.submit(t)] = t
59
+
40
60
  for future in as_completed(futures):
41
- msg = future.result()
42
- if msg:
43
- errors.append(futures[future])
61
+ task = futures[future]
62
+ ctx = task.args[0]
63
+
64
+ try:
65
+ result = future.result()
66
+
67
+ # Some renderers may return error messages instead of raising
68
+ if result:
69
+ errors.append({"source": ctx.source, "error": str(result)})
70
+
71
+ except Exception as e:
72
+ errors.append({"source": ctx.source, "error": str(e)})
73
+
44
74
  return errors
45
75
 
46
76
 
@@ -57,35 +87,24 @@ def find_files(root: Path, extension: str, excludes: list[str] | None) -> list[P
57
87
 
58
88
 
59
89
  def _get_extension(path: Path) -> str:
60
- return path.suffix.split(".")[1]
90
+ return path.suffix.lstrip(".")
61
91
 
62
92
 
63
93
  def _collect_sources(root: Path, formats: list[str], excludes: list[str]) -> list[Path]:
64
94
  files: list[Path] = []
65
95
  if root.is_file():
66
96
  logger.info("Using source: %s", root)
67
- suffix = _get_extension(root)
68
- if suffix not in _ENVIRONMENT_CHECKS:
69
- logger.warning("Unknown extension .%s — no renderer registered", suffix)
70
- return files
71
- if _ENVIRONMENT_CHECKS[suffix]():
72
- files.append(root)
73
- else:
74
- logger.warning(
75
- "Render environment doesn't support requested extension: %s", root
76
- )
97
+ files.append(root)
77
98
  else:
78
99
  logger.info("Searching for sources in: %s", root)
79
100
  if not formats:
80
101
  formats = _FORMATS
81
102
  for f in formats:
82
103
  if f not in _ENVIRONMENT_CHECKS:
83
- logger.warning("Unknown format requested: %s — skipping", f)
84
- continue
85
- if _ENVIRONMENT_CHECKS[f]():
86
- files.extend(find_files(root, f, excludes))
87
- else:
88
- logger.debug("Skipping .%s — renderer not available in environment", f)
104
+ logger.warning("Unknown format requested: %s — including anyway", f)
105
+
106
+ files.extend(find_files(root, f, excludes))
107
+
89
108
  return files
90
109
 
91
110
 
@@ -97,27 +116,49 @@ def _setup_tasks(
97
116
  for f in sources:
98
117
 
99
118
  ext = _get_extension(f)
100
- if config.output_formats:
101
- output_formats = config.output_formats
102
- else:
103
- output_formats = _DEFAULT_OUTPUT_FORMATS[ext]
119
+
120
+ output_formats = (
121
+ config.output_formats
122
+ if config.output_formats
123
+ else _DEFAULT_OUTPUT_FORMATS.get(ext, ["png"])
124
+ )
125
+
126
+ file_config = apply_frontmatter(config, parse_frontmatter(f))
104
127
 
105
128
  for fmt in output_formats:
129
+ ctx = RenderContext(
130
+ config=file_config,
131
+ source=f,
132
+ build_dir=build_dir,
133
+ output_dir=output_dir,
134
+ output_type=fmt,
135
+ )
136
+
137
+ # No renderer, return error
106
138
  if ext not in _RENDERERS:
107
- logger.warning("No renderer for extension .%s — skipping %s", ext, f)
139
+ logger.warning("No renderer for .%s — reporting error for %s", ext, f)
140
+ tasks.append(
141
+ partial(_fail_task, ctx, f"No renderer for extension: {ext}")
142
+ )
108
143
  continue
109
- tasks.append(
110
- partial(
111
- _RENDERERS[ext],
112
- RenderContext(
113
- config=config,
114
- source=f,
115
- build_dir=build_dir,
116
- output_dir=output_dir,
117
- output_type=fmt,
118
- ),
144
+
145
+ # Environment not supported, return error
146
+ if not _ENVIRONMENT_CHECKS.get(ext, lambda: False)():
147
+ logger.warning(
148
+ "Renderer unavailable for .%s — reporting error for %s", ext, f
119
149
  )
120
- )
150
+ tasks.append(
151
+ partial(
152
+ _fail_task,
153
+ ctx,
154
+ f"Environment not supported for extension: {ext}",
155
+ )
156
+ )
157
+ continue
158
+
159
+ # Normal rendering
160
+ tasks.append(partial(_RENDERERS[ext], ctx))
161
+
121
162
  return tasks
122
163
 
123
164
 
@@ -127,12 +168,11 @@ def render(
127
168
  config: RenderConfig,
128
169
  output_dir: Path | None = None,
129
170
  ):
130
-
131
171
  sources = _collect_sources(root, config.input_formats, config.excludes)
132
172
 
133
173
  if not sources:
134
174
  logger.warning("No renderable sources found under: %s", root)
135
- return
175
+ return []
136
176
 
137
177
  # There can be multiple tasks per source, if there are multiple output formats
138
178
  tasks = _setup_tasks(sources, config, build_dir, output_dir)
@@ -143,7 +183,9 @@ def render(
143
183
  succeeded = len(tasks) - len(errors)
144
184
  if errors:
145
185
  logger.error("%d/%d task(s) failed:", len(errors), len(tasks))
146
- for task_error in errors:
147
- logger.error(" %s", task_error.args[0].source.relative_to(root))
186
+ for err in errors:
187
+ logger.error(" %s: %s", err["source"].relative_to(root), err["error"])
148
188
  else:
149
189
  logger.info("Done — %d/%d task(s) succeeded", succeeded, len(tasks))
190
+
191
+ return errors
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: icplot
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Utilities for generating plots and graphics for technical reports.
5
5
  Author-email: "James Grogan, Irish Centre for High End Computing" <james.grogan@ichec.ie>
6
6
  Project-URL: Repository, https://git.ichec.ie/performance/toolshed/icplot
@@ -14,11 +14,6 @@ Classifier: Topic :: System :: Distributed Computing
14
14
  Requires-Python: >=3.10
15
15
  Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
- Requires-Dist: wand
18
- Requires-Dist: matplotlib
19
- Requires-Dist: vtk
20
- Requires-Dist: moviepy
21
- Requires-Dist: scipy
22
17
  Requires-Dist: iccore==0.2.3
23
18
  Requires-Dist: graphviz
24
19
  Requires-Dist: Jinja2
@@ -31,9 +26,29 @@ Requires-Dist: isort; extra == "test"
31
26
  Requires-Dist: mypy; extra == "test"
32
27
  Requires-Dist: flake8; extra == "test"
33
28
  Requires-Dist: pylint; extra == "test"
29
+ Requires-Dist: types-PyYAML; extra == "test"
34
30
  Provides-Extra: cairo
35
- Requires-Dist: pycairo; extra == "cairo"
36
31
  Requires-Dist: CairoSVG; extra == "cairo"
32
+ Provides-Extra: scene
33
+ Requires-Dist: pycairo; extra == "scene"
34
+ Provides-Extra: imagemagick
35
+ Requires-Dist: wand; extra == "imagemagick"
36
+ Provides-Extra: plot
37
+ Requires-Dist: matplotlib; extra == "plot"
38
+ Provides-Extra: vtk
39
+ Requires-Dist: vtk; extra == "vtk"
40
+ Provides-Extra: geometry
41
+ Requires-Dist: scipy; extra == "geometry"
42
+ Provides-Extra: video
43
+ Requires-Dist: moviepy; extra == "video"
44
+ Provides-Extra: all
45
+ Requires-Dist: CairoSVG; extra == "all"
46
+ Requires-Dist: pycairo; extra == "all"
47
+ Requires-Dist: wand; extra == "all"
48
+ Requires-Dist: matplotlib; extra == "all"
49
+ Requires-Dist: vtk; extra == "all"
50
+ Requires-Dist: scipy; extra == "all"
51
+ Requires-Dist: moviepy; extra == "all"
37
52
  Dynamic: license-file
38
53
 
39
54
  # icplot
@@ -121,13 +136,30 @@ The library supports themeing with an Angular Material 3 motivated design system
121
136
 
122
137
  # Installation #
123
138
 
124
- The package is available on PyPI. For a minimal installation you can do:
139
+ The package is available on PyPI. The core install is deliberately light — it
140
+ does not pull the heavy rendering backends, so consumers that only need part of
141
+ the library (e.g. a docs build that just converts svg to png) stay small:
125
142
 
126
143
  ``` shell
127
144
  pip install icplot
128
145
  ```
129
146
 
130
- For full functionality, particularly conversion of image formats, `imagemagick` and `cairo` are required. On Mac you can install them with:
147
+ Backends are opt-in via extras:
148
+
149
+ | Extra | Pulls | Enables |
150
+ |---|---|---|
151
+ | `cairo` | CairoSVG | svg → png / pdf conversion |
152
+ | `scene` | pycairo | vector scene drawing (`renderer.scene.cairo`) |
153
+ | `imagemagick` | wand | pdf → png conversion (falls back to the `magick` CLI) |
154
+ | `plot` | matplotlib | 2D plotting (`renderer.graph`, `charts`) |
155
+ | `vtk` | vtk | 3D / mesh rendering (`geometry.mesh`) |
156
+ | `geometry` | scipy | geometry transforms (`geometry.operations`) |
157
+ | `video` | moviepy | video assembly (`renderer.video`) |
158
+ | `all` | all of the above | everything (previous default behaviour) |
159
+
160
+ For example, `pip install icplot[cairo]` for svg conversion, or
161
+ `pip install icplot[all]` for the full toolkit. The native `cairo` and
162
+ `imagemagick` system libraries are also required for those backends; on Mac:
131
163
 
132
164
  ``` shell
133
165
  brew install imagemagick cairo
@@ -43,6 +43,7 @@ src/icplot/graph/series.py
43
43
  src/icplot/renderer/__init__.py
44
44
  src/icplot/renderer/cli.py
45
45
  src/icplot/renderer/config.py
46
+ src/icplot/renderer/frontmatter.py
46
47
  src/icplot/renderer/render.py
47
48
  src/icplot/renderer/video.py
48
49
  src/icplot/renderer/charts/pgfgantt.py
@@ -67,6 +68,7 @@ src/icplot/theme/theme_generator.py
67
68
  src/icplot/theme/templates/puml.j2
68
69
  src/icplot/theme/templates/tikz.j2
69
70
  test/test_cairo.py
71
+ test/test_frontmatter.py
70
72
  test/test_gantt_chart.py
71
73
  test/test_geometry.py
72
74
  test/test_image.py
@@ -1,16 +1,31 @@
1
+ iccore==0.2.3
2
+ graphviz
3
+ Jinja2
4
+
5
+ [all]
6
+ CairoSVG
7
+ pycairo
1
8
  wand
2
9
  matplotlib
3
10
  vtk
4
- moviepy
5
11
  scipy
6
- iccore==0.2.3
7
- graphviz
8
- Jinja2
12
+ moviepy
9
13
 
10
14
  [cairo]
11
- pycairo
12
15
  CairoSVG
13
16
 
17
+ [geometry]
18
+ scipy
19
+
20
+ [imagemagick]
21
+ wand
22
+
23
+ [plot]
24
+ matplotlib
25
+
26
+ [scene]
27
+ pycairo
28
+
14
29
  [test]
15
30
  pytest
16
31
  pytest-cov
@@ -20,3 +35,10 @@ isort
20
35
  mypy
21
36
  flake8
22
37
  pylint
38
+ types-PyYAML
39
+
40
+ [video]
41
+ moviepy
42
+
43
+ [vtk]
44
+ vtk
@@ -0,0 +1,52 @@
1
+ from pathlib import Path
2
+
3
+ from iccore.test_utils import get_test_data_dir
4
+
5
+ from icplot.renderer.config import PlantUmlConfig, RenderConfig, apply_frontmatter
6
+ from icplot.renderer.frontmatter import parse
7
+
8
+
9
+ def test_parse_returns_dict():
10
+ fm = parse(get_test_data_dir() / "test_frontmatter.puml")
11
+ assert fm["theme"] == "none"
12
+ assert fm["meta"]["description"] == "Frontmatter parse test"
13
+
14
+
15
+ def test_parse_no_frontmatter():
16
+ fm = parse(get_test_data_dir() / "test.puml")
17
+ assert fm == {}
18
+
19
+
20
+ def test_parse_missing_file():
21
+ fm = parse(Path("/nonexistent/file.puml"))
22
+ assert fm == {}
23
+
24
+
25
+ def test_apply_theme_none_clears_config_path(tmp_path):
26
+ config_file = tmp_path / "theme.puml"
27
+ config_file.touch()
28
+ config = RenderConfig(plantuml=PlantUmlConfig(config_path=config_file))
29
+
30
+ result = apply_frontmatter(config, {"theme": "none"})
31
+
32
+ assert result.plantuml.config_path is None
33
+
34
+
35
+ def test_apply_empty_frontmatter_leaves_config_unchanged(tmp_path):
36
+ config_file = tmp_path / "theme.puml"
37
+ config_file.touch()
38
+ config = RenderConfig(plantuml=PlantUmlConfig(config_path=config_file))
39
+
40
+ result = apply_frontmatter(config, {})
41
+
42
+ assert result.plantuml.config_path == config_file
43
+
44
+
45
+ def test_apply_meta_only_leaves_config_unchanged(tmp_path):
46
+ config_file = tmp_path / "theme.puml"
47
+ config_file.touch()
48
+ config = RenderConfig(plantuml=PlantUmlConfig(config_path=config_file))
49
+
50
+ result = apply_frontmatter(config, {"meta": {"description": "test"}})
51
+
52
+ assert result.plantuml.config_path == config_file
@@ -0,0 +1,37 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+ from iccore.test_utils import get_test_output_dir
6
+
7
+ from icplot.converter.image import _pdf_to_png, svg_to_pdf, svg_to_png
8
+
9
+
10
+ def get_test_data_dir():
11
+ return Path(__file__).parent / "data"
12
+
13
+
14
+ def test_convert_svg():
15
+
16
+ source = get_test_data_dir() / "test.svg"
17
+
18
+ png_out = Path(os.getcwd()) / "out.png"
19
+ svg_to_png(source, png_out)
20
+ png_out.unlink()
21
+
22
+ pdf_out = Path(os.getcwd()) / "out.pdf"
23
+ svg_to_pdf(source, pdf_out)
24
+ pdf_out.unlink()
25
+
26
+
27
+ def test_convert_pdf():
28
+
29
+ source = get_test_data_dir() / "test.pdf"
30
+ output_dir = get_test_output_dir()
31
+ target = output_dir / "test.png"
32
+
33
+ _pdf_to_png(source, target)
34
+
35
+ assert target.exists()
36
+ assert target.stat().st_size > 0
37
+ target.unlink()
@@ -1,41 +0,0 @@
1
- from pathlib import Path
2
-
3
- from pydantic import BaseModel
4
-
5
-
6
- class TexConfig(BaseModel, frozen=True):
7
- """
8
- This holds settings for the Tex build
9
- """
10
-
11
- build_engine: str = "pdflatex"
12
- nonstopmode: bool = True
13
- use_woff_fonts: bool = True
14
- templates: list[Path] = []
15
-
16
-
17
- class PlantUmlConfig(BaseModel, frozen=True):
18
- config_path: Path | None = None
19
-
20
-
21
- class MermaidConfig(BaseModel, frozen=True):
22
- browser_config: Path | None = None
23
-
24
-
25
- class RenderConfig(BaseModel, frozen=True):
26
- tex: TexConfig = TexConfig()
27
- plantuml: PlantUmlConfig = PlantUmlConfig()
28
- mermaid: MermaidConfig = MermaidConfig()
29
- extra_config: Path | None = None
30
- input_formats: list[str] = []
31
- output_formats: list[str] = []
32
- excludes: list[str] = []
33
-
34
-
35
- class RenderContext(BaseModel, frozen=True):
36
- config: RenderConfig
37
- source: Path
38
- output_type: str
39
- build_dir: Path
40
- output_dir: Path | None = None
41
- clear_workdir: bool = True
@@ -1,21 +0,0 @@
1
- import os
2
- from pathlib import Path
3
-
4
- from icplot.converter.image import svg_to_pdf, svg_to_png
5
-
6
-
7
- def get_test_data_dir():
8
- return Path(__file__).parent / "data"
9
-
10
-
11
- def test_convert_svg():
12
-
13
- source = get_test_data_dir() / "test.svg"
14
-
15
- png_out = Path(os.getcwd()) / "out.png"
16
- svg_to_png(source, png_out)
17
- png_out.unlink()
18
-
19
- pdf_out = Path(os.getcwd()) / "out.pdf"
20
- svg_to_pdf(source, pdf_out)
21
- pdf_out.unlink()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes