icplot 0.3.3__tar.gz → 0.3.5__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.3.5}/PKG-INFO +2 -1
  2. {icplot-0.3.3 → icplot-0.3.5}/pyproject.toml +5 -4
  3. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/converter/image.py +6 -1
  4. icplot-0.3.5/src/icplot/renderer/config.py +73 -0
  5. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/formats/tex.py +3 -1
  6. icplot-0.3.5/src/icplot/renderer/frontmatter.py +62 -0
  7. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/matplotlib.py +2 -2
  8. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/render.py +86 -44
  9. {icplot-0.3.3 → icplot-0.3.5/src/icplot.egg-info}/PKG-INFO +2 -1
  10. {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/SOURCES.txt +2 -0
  11. {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/requires.txt +1 -0
  12. icplot-0.3.5/test/test_frontmatter.py +52 -0
  13. icplot-0.3.5/test/test_image.py +37 -0
  14. icplot-0.3.3/src/icplot/renderer/config.py +0 -41
  15. icplot-0.3.3/test/test_image.py +0 -21
  16. {icplot-0.3.3 → icplot-0.3.5}/LICENSE +0 -0
  17. {icplot-0.3.3 → icplot-0.3.5}/README.md +0 -0
  18. {icplot-0.3.3 → icplot-0.3.5}/setup.cfg +0 -0
  19. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/__init__.py +0 -0
  20. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/charts/__init__.py +0 -0
  21. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/charts/gantt.py +0 -0
  22. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/charts/tree.py +0 -0
  23. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/converter/__init__.py +0 -0
  24. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/converter/cli.py +0 -0
  25. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/converter/convert.py +0 -0
  26. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/__init__.py +0 -0
  27. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/base.py +0 -0
  28. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/__init__.py +0 -0
  29. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/cell.py +0 -0
  30. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/edge.py +0 -0
  31. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/mesh.py +0 -0
  32. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/openfoam/__init__.py +0 -0
  33. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/openfoam/blockmesh.py +0 -0
  34. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/openfoam/foamfile.py +0 -0
  35. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/openfoam/polymesh.py +0 -0
  36. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/operations.py +0 -0
  37. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/shapes.py +0 -0
  38. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/vertex.py +0 -0
  39. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/vtk.py +0 -0
  40. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/operations.py +0 -0
  41. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/shapes.py +0 -0
  42. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/transform.py +0 -0
  43. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/__init__.py +0 -0
  44. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/axis.py +0 -0
  45. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/plot.py +0 -0
  46. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/plot_group.py +0 -0
  47. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/series.py +0 -0
  48. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/main_cli.py +0 -0
  49. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/py.typed +0 -0
  50. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/__init__.py +0 -0
  51. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/charts/pgfgantt.py +0 -0
  52. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/charts/svggantt.py +0 -0
  53. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/cli.py +0 -0
  54. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/formats/__init__.py +0 -0
  55. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/formats/mermaid.py +0 -0
  56. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/formats/plantuml.py +0 -0
  57. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/__init__.py +0 -0
  58. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/color.py +0 -0
  59. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/mpl.py +0 -0
  60. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/renderer.py +0 -0
  61. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/vtk.py +0 -0
  62. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/scene/cairo.py +0 -0
  63. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/video.py +0 -0
  64. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/scene.py +0 -0
  65. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/__init__.py +0 -0
  66. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/cli.py +0 -0
  67. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/color.py +0 -0
  68. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/templates/puml.j2 +0 -0
  69. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/templates/tikz.j2 +0 -0
  70. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/theme.json +0 -0
  71. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/theme.py +0 -0
  72. {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/theme_generator.py +0 -0
  73. {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/dependency_links.txt +0 -0
  74. {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/entry_points.txt +0 -0
  75. {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/top_level.txt +0 -0
  76. {icplot-0.3.3 → icplot-0.3.5}/test/test_cairo.py +0 -0
  77. {icplot-0.3.3 → icplot-0.3.5}/test/test_gantt_chart.py +0 -0
  78. {icplot-0.3.3 → icplot-0.3.5}/test/test_geometry.py +0 -0
  79. {icplot-0.3.3 → icplot-0.3.5}/test/test_line_plots.py +0 -0
  80. {icplot-0.3.3 → icplot-0.3.5}/test/test_mermaid.py +0 -0
  81. {icplot-0.3.3 → icplot-0.3.5}/test/test_meshing.py +0 -0
  82. {icplot-0.3.3 → icplot-0.3.5}/test/test_openfoam.py +0 -0
  83. {icplot-0.3.3 → icplot-0.3.5}/test/test_plot_group.py +0 -0
  84. {icplot-0.3.3 → icplot-0.3.5}/test/test_polymesh.py +0 -0
  85. {icplot-0.3.3 → icplot-0.3.5}/test/test_puml.py +0 -0
  86. {icplot-0.3.3 → icplot-0.3.5}/test/test_tex.py +0 -0
  87. {icplot-0.3.3 → icplot-0.3.5}/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.3.5
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
@@ -31,6 +31,7 @@ Requires-Dist: isort; extra == "test"
31
31
  Requires-Dist: mypy; extra == "test"
32
32
  Requires-Dist: flake8; extra == "test"
33
33
  Requires-Dist: pylint; extra == "test"
34
+ Requires-Dist: types-PyYAML; extra == "test"
34
35
  Provides-Extra: cairo
35
36
  Requires-Dist: pycairo; extra == "cairo"
36
37
  Requires-Dist: CairoSVG; extra == "cairo"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "icplot"
3
- version = "0.3.3"
3
+ version = "0.3.5"
4
4
  authors = [
5
5
  { name="James Grogan, Irish Centre for High End Computing", email="james.grogan@ichec.ie" },
6
6
  ]
@@ -38,7 +38,8 @@ test = [
38
38
  "isort",
39
39
  "mypy",
40
40
  "flake8",
41
- "pylint"
41
+ "pylint",
42
+ "types-PyYAML"
42
43
  ]
43
44
 
44
45
  cairo = [
@@ -113,7 +114,7 @@ addopts = "--cov=icplot --cov-report term --cov-fail-under=55 --cov-report xml:c
113
114
 
114
115
  [tool.tox]
115
116
  requires = ["tox"]
116
- env_list = ["format_check", "lint", "style", "type", "3.10", "3.13", "docs"]
117
+ env_list = ["format_check", "lint", "style", "type", "3.10", "3.14", "docs"]
117
118
  skip_missing_interpreters = true
118
119
 
119
120
  [tool.tox.env_run_base]
@@ -153,7 +154,7 @@ commands = [["flake8", "src"]]
153
154
 
154
155
  [tool.tox.env.type]
155
156
  description = "Run Type Checks"
156
- deps = ["mypy", ".[types]"]
157
+ deps = ["mypy", ".[test]"]
157
158
  commands = [["mypy", "src"]]
158
159
 
159
160
  [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.3.5
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
@@ -31,6 +31,7 @@ Requires-Dist: isort; extra == "test"
31
31
  Requires-Dist: mypy; extra == "test"
32
32
  Requires-Dist: flake8; extra == "test"
33
33
  Requires-Dist: pylint; extra == "test"
34
+ Requires-Dist: types-PyYAML; extra == "test"
34
35
  Provides-Extra: cairo
35
36
  Requires-Dist: pycairo; extra == "cairo"
36
37
  Requires-Dist: CairoSVG; extra == "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
@@ -20,3 +20,4 @@ isort
20
20
  mypy
21
21
  flake8
22
22
  pylint
23
+ types-PyYAML
@@ -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
File without changes