Skip to content

Commit

Permalink
Introduce a Configuration type
Browse files Browse the repository at this point in the history
We need to make an effort to type the BasePlugin.config dict, as we're
otherwise having to unpack and pass many arguments to some of the more
complicated callbacks.

I'm trying to straddle the line between a reasonable number of args and
overly tight coupling by only using this object in methods which
actually do processing for now, and not those involved in computing
configuration.
  • Loading branch information
LukeCarrier committed Apr 16, 2024
1 parent 64366d1 commit 02cf0e9
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 37 deletions.
43 changes: 30 additions & 13 deletions mkdocs_drawio_exporter/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,29 @@
import shutil
import subprocess
import sys
from typing import TypedDict


IMAGE_RE = re.compile('(<img[^>]+src=")([^">]+)("\\s*\\/?>)')


class Configuration(TypedDict):
"""Draw.io Exporter MkDocs plugin configuration.
Contains the resolved configuration values, including defaults and any
computed values (such as paths to binaries).
Seems ugly to shadow BasePlugin.config_scheme, but improves type hints and
allows us to more easily pass configuration around."""

cache_dir: str
drawio_executable: str
drawio_args: list[str]
format: str
embed_format: str
sources: str


class ConfigurationError(Exception):
"""Configuration exception.
Expand Down Expand Up @@ -203,7 +221,7 @@ def prepare_drawio_executable(self, executable, executable_names, platform_execu
raise ConfigurationError.drawio_executable(
None, 'Unable to find Draw.io executable; ensure it\'s on PATH or set drawio_executable option')

def rewrite_image_embeds(self, output_content, sources, format, embed_format):
def rewrite_image_embeds(self, output_content, config: Configuration):
"""Rewrite image embeds.
:param str output_content: Content to rewrite.
Expand All @@ -221,11 +239,11 @@ def replace(match):
filename = match.group(2)
page_index = 0

if fnmatch.fnmatch(filename, sources):
if fnmatch.fnmatch(filename, config["sources"]):
content_sources.append(Source(filename, page_index))
img_src = f"{filename}-{page_index}.{format}"
img_src = f"{filename}-{page_index}.{config["format"]}"

return embed_format.format(
return config["embed_format"].format(
img_open=match.group(1), img_close=match.group(3),
img_src=img_src)
else:
Expand All @@ -243,7 +261,7 @@ def filter_cache_files(self, files, cache_dir):
"""
return [f for f in files if not f.abs_src_path.startswith(cache_dir)]

def ensure_file_cached(self, source, source_rel, page_index, drawio_executable, drawio_args, cache_dir, format):
def ensure_file_cached(self, source, source_rel, page_index, config: Configuration):
"""Ensure cached copy of output exists.
:param str source: Source path, absolute.
Expand All @@ -255,20 +273,19 @@ def ensure_file_cached(self, source, source_rel, page_index, drawio_executable,
:param str format: Desired export format.
:return tuple(str, int): Cached export filename.
"""
cache_filename = self.make_cache_filename(source_rel, page_index, cache_dir)
cache_filename = self.make_cache_filename(source_rel, page_index, config['cache_dir'])
exit_status = None

if self.use_cached_file(source, cache_filename):
self.log.debug(f'Source file appears unchanged; using cached copy from "{cache_filename}"')
else:
if not drawio_executable:
if not config['drawio_executable']:
self.log.warning(f'Skipping export of "{source}" as Draw.io executable not available')
return (None, exit_status)

self.log.debug(f'Exporting "{source}" to "{cache_filename}"')
exit_status = self.export_file(
source, page_index, cache_filename,
drawio_executable, drawio_args, format)
source, page_index, cache_filename, config)

return (cache_filename, exit_status)

Expand All @@ -294,7 +311,7 @@ def use_cached_file(self, source, cache_filename):
return os.path.exists(cache_filename) \
and os.path.getmtime(cache_filename) >= os.path.getmtime(source)

def export_file(self, source, page_index, dest, drawio_executable, drawio_args, format):
def export_file(self, source, page_index, dest, config: Configuration):
"""Export an individual file.
:param str source: Source path, absolute.
Expand All @@ -306,13 +323,13 @@ def export_file(self, source, page_index, dest, drawio_executable, drawio_args,
:return int: The Draw.io exit status.
"""
cmd = [
drawio_executable,
config['drawio_executable'],
'--export', source,
'--page-index', str(page_index),
'--output', dest,
'--format', format,
'--format', config['format'],
]
cmd += drawio_args
cmd += config['drawio_args']

try:
self.log.debug(f'Using export command {cmd}')
Expand Down
8 changes: 3 additions & 5 deletions mkdocs_drawio_exporter/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from mkdocs.structure.files import Files
from mkdocs.utils import copy_file

from .exporter import ConfigurationError, DrawIoExporter, Source
from .exporter import ConfigurationError, DrawIoExporter, Configuration


log = mkdocs.plugins.log.getChild('drawio-exporter')
Expand Down Expand Up @@ -54,8 +54,7 @@ def on_config(self, config):

def on_post_page(self, output_content, page, **kwargs):
output_content, content_sources = self.exporter.rewrite_image_embeds(
output_content, self.config['sources'],
self.config['format'], self.config['embed_format'])
output_content, self.config)

for source in content_sources:
source.resolve_rel_path(page.file.dest_path)
Expand All @@ -80,8 +79,7 @@ def on_post_build(self, config):
abs_dest_path = os.path.join(config['site_dir'], dest_rel_path)
cache_filename, exit_status = self.exporter.ensure_file_cached(
abs_src_path, source.source_rel, source.page_index,
self.config['drawio_executable'], self.config['drawio_args'],
self.config['cache_dir'], self.config['format'])
self.config)

if exit_status not in (None, 0):
log.error(f'Export failed with exit status {exit_status}; skipping copy')
Expand Down
86 changes: 67 additions & 19 deletions mkdocs_drawio_exporter/tests/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from os.path import isabs, join, sep
import re

from ..exporter import ConfigurationError, DrawIoExporter
from ..exporter import Configuration, ConfigurationError, DrawIoExporter


class FileMock:
Expand All @@ -26,6 +26,20 @@ def setUp(self):
self.log = logging.getLogger(__name__)
self.exporter = DrawIoExporter(self.log)

def make_config(self, **kwargs):
defaults = {
'cache_dir': 'drawio-exporter',
'drawio_executable': 'drawio',
'drawio_args': [],
'format': 'svg',
'embed_format': '{img_open}{img_src}{img_close}',
'sources': '*.drawio',
}
# FIXME: when dropping support for Python 3.8, replace with the merge
# operator (|).
values = {**defaults, **kwargs}
return Configuration(**values)

def test_drawio_executable_paths_warns_on_unknown_platform(self):
self.log.warning = MagicMock()
self.exporter.drawio_executable_paths('win32-but-stable')
Expand Down Expand Up @@ -77,23 +91,24 @@ def test_prepare_drawio_executable_raises_on_failure(self):
def test_rewrite_image_embeds(self):
source = '''<h1>Example text</h1>
<img alt="Some text" src="../some-diagram.drawio" />'''

default_embed_format = '{img_open}{img_src}{img_close}'
object_embed_format = '<object type="image/svg+xml" data="{img_src}"></object>'

config = self.make_config(sources='*.nomatch')
output_content, sources = self.exporter.rewrite_image_embeds(
source, '*.nomatch', 'svg', default_embed_format)
source, config)
assert output_content == source
assert sources == []

config = self.make_config()
output_content, sources = self.exporter.rewrite_image_embeds(
source, '*.drawio', 'svg', default_embed_format)
source, config)
assert output_content != source
assert 'src="../some-diagram.drawio-0.svg"' in output_content
assert len(sources) == 1

config = self.make_config(embed_format=object_embed_format)
output_content, sources = self.exporter.rewrite_image_embeds(
source, '*.drawio', 'svg', object_embed_format)
source, config)
assert output_content != source
assert '<object type="image/svg+xml" data="../some-diagram.drawio-0.svg"></object>' in output_content
assert len(sources) == 1
Expand All @@ -118,14 +133,19 @@ def test_ensure_file_cached(self):
drawio_executable = sep + join('bin', 'drawio')
cache_dir = sep + join('docs', 'drawio-exporter')

config = self.make_config(
cache_dir=cache_dir,
drawio_executable=drawio_executable,
)

self.exporter.make_cache_filename = MagicMock()
self.exporter.make_cache_filename.return_value = join(cache_dir, '0000000000000000000000000000000000000000-0')

self.exporter.export_file = MagicMock()
self.exporter.export_file.return_value = 0

cache_filename, exit_status = self.exporter.ensure_file_cached(
source, source_rel, 0, drawio_executable, [], cache_dir, 'svg')
source, source_rel, 0, config)
assert cache_filename == self.exporter.make_cache_filename.return_value
assert exit_status == 0

Expand All @@ -135,13 +155,18 @@ def test_ensure_file_cached_aborts_if_drawio_executable_unavailable(self):
drawio_executable = None
cache_dir = sep + join('docs', 'drawio-exporter')

config = self.make_config(
cache_dir=cache_dir,
drawio_executable=drawio_executable,
)

self.exporter.export_file = MagicMock()
self.exporter.export_file.return_value = 0

self.log.warning = MagicMock()

cache_filename, exit_status = self.exporter.ensure_file_cached(
source, source_rel, 0, drawio_executable, [], cache_dir, 'svg')
source, source_rel, 0, config)

assert exit_status == None
self.log.warning.assert_called_once()
Expand All @@ -152,6 +177,11 @@ def test_ensure_file_cached_skips_export_if_cache_fresh(self):
drawio_executable = sep + join('bin', 'drawio')
cache_dir = sep + join('docs', 'drawio-exporter')

config = self.make_config(
cache_dir=cache_dir,
drawio_executable=drawio_executable,
)

self.exporter.make_cache_filename = MagicMock()
self.exporter.make_cache_filename.return_value = join(cache_dir, '0000000000000000000000000000000000000000-0')

Expand All @@ -162,7 +192,7 @@ def test_ensure_file_cached_skips_export_if_cache_fresh(self):
self.exporter.export_file.return_value = 0

cache_filename, exit_status = self.exporter.ensure_file_cached(
source, source_rel, 0, drawio_executable, [], cache_dir, 'svg')
source, source_rel, 0, config)

assert cache_filename == self.exporter.make_cache_filename.return_value
assert exit_status == None
Expand All @@ -175,6 +205,11 @@ def test_ensure_file_cached_returns_exit_status_if_non_zero(self):
drawio_executable = sep + join('bin', 'drawio')
cache_dir = sep + join('docs', 'drawio-exporter')

config = self.make_config(
cache_dir=cache_dir,
drawio_executable=drawio_executable,
)

self.exporter.make_cache_filename = MagicMock()
self.exporter.make_cache_filename.return_value = join(cache_dir, '0000000000000000000000000000000000000000-0')

Expand All @@ -187,7 +222,7 @@ def test_ensure_file_cached_returns_exit_status_if_non_zero(self):
self.log.error = MagicMock()

cache_filename, exit_status = self.exporter.ensure_file_cached(
source, source_rel, 0, drawio_executable, [], cache_dir, 'svg')
source, source_rel, 0, config)

assert exit_status == 1

Expand Down Expand Up @@ -230,10 +265,14 @@ def test_export_file(self, call_mock):
dest = sep + join('docs', 'diagram.drawio-0.svg')
drawio_executable = sep + join('bin', 'drawio')

config = self.make_config(
drawio_executable=drawio_executable,
)

call_mock.return_value = 0

result = self.exporter.export_file(
source, 0, dest, drawio_executable, [], 'svg')
source, 0, dest, config)

assert result == 0
call_mock.assert_called_once()
Expand All @@ -244,12 +283,16 @@ def test_export_file_logs_exc_on_raise(self, call_mock):
dest = sep + join('docs', 'diagram.drawio-0.svg')
drawio_executable = sep + join('bin', 'drawio')

config = self.make_config(
drawio_executable=drawio_executable,
)

call_mock.side_effect = OSError()

self.log.exception = MagicMock()

result = self.exporter.export_file(
source, 0, dest, drawio_executable, [], 'svg')
source, 0, dest, config)

assert result == None
self.log.exception.assert_called_once()
Expand All @@ -261,21 +304,26 @@ def test_export_file_honours_drawio_args(self, call_mock):
page_index = 0
dest = sep + join('docs', 'diagram.drawio-0.svg')
drawio_executable = sep + join('bin', 'drawio')
format = 'svg'

def test_drawio_args(drawio_args):
config = self.make_config(
drawio_executable=drawio_executable,
format='svg'
)

def test_drawio_args(config: Configuration, drawio_args):
test_config = {**config, 'drawio_args': drawio_args}
self.exporter.export_file(
source, page_index, dest, drawio_executable, drawio_args, format)
source, page_index, dest, test_config)
call_mock.assert_called_with([
drawio_executable,
config['drawio_executable'],
'--export', source,
'--page-index', str(page_index),
'--output', dest,
'--format', format,
'--format', config['format'],
] + drawio_args)

test_drawio_args([])
test_drawio_args(['--no-sandbox'])
test_drawio_args(config, [])
test_drawio_args(config, ['--no-sandbox'])


if __name__ == '__main__':
Expand Down

0 comments on commit 02cf0e9

Please sign in to comment.