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.
- {icplot-0.3.3/src/icplot.egg-info → icplot-0.3.5}/PKG-INFO +2 -1
- {icplot-0.3.3 → icplot-0.3.5}/pyproject.toml +5 -4
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/converter/image.py +6 -1
- icplot-0.3.5/src/icplot/renderer/config.py +73 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/formats/tex.py +3 -1
- icplot-0.3.5/src/icplot/renderer/frontmatter.py +62 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/matplotlib.py +2 -2
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/render.py +86 -44
- {icplot-0.3.3 → icplot-0.3.5/src/icplot.egg-info}/PKG-INFO +2 -1
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/SOURCES.txt +2 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/requires.txt +1 -0
- icplot-0.3.5/test/test_frontmatter.py +52 -0
- icplot-0.3.5/test/test_image.py +37 -0
- icplot-0.3.3/src/icplot/renderer/config.py +0 -41
- icplot-0.3.3/test/test_image.py +0 -21
- {icplot-0.3.3 → icplot-0.3.5}/LICENSE +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/README.md +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/setup.cfg +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/charts/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/charts/gantt.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/charts/tree.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/converter/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/converter/cli.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/converter/convert.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/base.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/cell.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/edge.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/mesh.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/openfoam/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/openfoam/blockmesh.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/openfoam/foamfile.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/openfoam/polymesh.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/operations.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/shapes.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/vertex.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/mesh/vtk.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/operations.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/shapes.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/geometry/transform.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/axis.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/plot.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/plot_group.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/graph/series.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/main_cli.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/py.typed +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/charts/pgfgantt.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/charts/svggantt.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/cli.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/formats/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/formats/mermaid.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/formats/plantuml.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/color.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/mpl.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/renderer.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/graph/vtk.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/scene/cairo.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/renderer/video.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/scene.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/__init__.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/cli.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/color.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/templates/puml.j2 +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/templates/tikz.j2 +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/theme.json +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/theme.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot/theme/theme_generator.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/dependency_links.txt +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/entry_points.txt +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/src/icplot.egg-info/top_level.txt +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_cairo.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_gantt_chart.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_geometry.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_line_plots.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_mermaid.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_meshing.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_openfoam.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_plot_group.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_polymesh.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_puml.py +0 -0
- {icplot-0.3.3 → icplot-0.3.5}/test/test_tex.py +0 -0
- {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
|
+
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
|
+
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.
|
|
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", ".[
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
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 = {
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
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
|
-
|
|
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 —
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
output_formats
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
147
|
-
logger.error(" %s",
|
|
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
|
+
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
|
|
@@ -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
|
icplot-0.3.3/test/test_image.py
DELETED
|
@@ -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
|
|
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
|
|
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
|