Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add linting #152

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ jobs:

- name: Install dependencies
run: |
python -m pip install -U pytest
python -m pip install -e .[test]

- name: Lint
run: |
ruff format --check sphinxcontrib
ruff check sphinxcontrib

- name: Test
run: |
pytest
20 changes: 20 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[tool.ruff]
line-length = 150

[tool.ruff.lint]
extend-select = ["I"]

[tool.ruff.lint.isort]
combine-as-imports = true
default-section = "third-party"
known-first-party = ["sphinxcontrib.mermaid"]
section-order = [
"future",
"standard-library",
"third-party",
"first-party",
"local-folder",
]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401", "F403"]
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ def remove_block(text, token, margin=0):
"Topic :: Utilities",
],
platforms="any",
packages=find_namespace_packages(where='./', include=['sphinxcontrib.mermaid']),
package_dir={'': './'},
packages=find_namespace_packages(where="./", include=["sphinxcontrib.mermaid"]),
package_dir={"": "./"},
include_package_data=True,
install_requires=["sphinx", "pyyaml"],
extras_require={'test': ['myst-parser', 'defusedxml', 'sphinx', 'pytest']},
extras_require={"test": ["defusedxml", "myst-parser", "pytest", "ruff", "sphinx"]},
)
137 changes: 48 additions & 89 deletions sphinxcontrib/mermaid/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""
sphinx-mermaid
~~~~~~~~~~~~~~~
sphinx-mermaid
~~~~~~~~~~~~~~~

Allow mermaid diagrams to be included in Sphinx-generated
documents inline.
Allow mermaid diagrams to be included in Sphinx-generated
documents inline.

:copyright: Copyright 2016-2023 by Martín Gaitán and others
:license: BSD, see LICENSE for details.
:copyright: Copyright 2016-2023 by Martín Gaitán and others
:license: BSD, see LICENSE for details.
"""

from __future__ import annotations

import codecs
Expand Down Expand Up @@ -80,6 +81,7 @@
window.addEventListener("load", load);
"""


class mermaid(nodes.General, nodes.Inline, nodes.Element):
pass

Expand All @@ -90,9 +92,7 @@ def figure_wrapper(directive, node, caption):
figure_node["align"] = node.attributes.pop("align")

parsed = nodes.Element()
directive.state.nested_parse(
ViewList([caption], source=""), directive.content_offset, parsed
)
directive.state.nested_parse(ViewList([caption], source=""), directive.content_offset, parsed)
caption_node = nodes.caption(parsed[0].rawsource, "", *parsed[0].children)
caption_node.source = parsed[0].source
caption_node.line = parsed[0].line
Expand Down Expand Up @@ -132,8 +132,7 @@ def get_mm_code(self):
if self.content:
return [
document.reporter.warning(
"Mermaid directive cannot have both content and "
"a filename argument",
"Mermaid directive cannot have both content and " "a filename argument",
line=self.lineno,
)
]
Expand All @@ -147,8 +146,7 @@ def get_mm_code(self):
except OSError:
return [
document.reporter.warning(
"External Mermaid file %r not found or reading "
"it failed" % filename,
"External Mermaid file %r not found or reading " "it failed" % filename,
line=self.lineno,
)
]
Expand Down Expand Up @@ -192,7 +190,7 @@ def run(self, **kwargs):
mm_config = "---"
if "config" in self.options:
mm_config += "\n"
mm_config += dump({"config": loads(self.options['config'])})
mm_config += dump({"config": loads(self.options["config"])})
if "title" in self.options:
mm_config += "\n"
mm_config += f"title: {self.options['title']}"
Expand All @@ -209,7 +207,6 @@ def run(self, **kwargs):


class MermaidClassDiagram(Mermaid):

has_content = False
required_arguments = 1
optional_arguments = 100
Expand Down Expand Up @@ -239,17 +236,15 @@ def render_mm(self, code, options, _fmt, prefix="mermaid"):

mermaid_cmd = self.builder.config.mermaid_cmd
mermaid_cmd_shell = self.builder.config.mermaid_cmd_shell in {True, "True", "true"}
hashkey = (
code + str(options) + str(self.builder.config.mermaid_sequence_config)
).encode("utf-8")
hashkey = (code + str(options) + str(self.builder.config.mermaid_sequence_config)).encode("utf-8")

basename = f"{prefix}-{sha1(hashkey).hexdigest()}"
fname = f"{basename}.{_fmt}"
relfn = posixpath.join(self.builder.imgpath, fname)
outdir = os.path.join(self.builder.outdir, self.builder.imagedir)
outfn = os.path.join(outdir, fname)
with TemporaryDirectory() as tempDir:
tmpfn = os.path.join(tempDir, basename)
tmpfn = os.path.join(tempDir, basename)

if os.path.isfile(outfn):
return relfn, outfn
Expand All @@ -265,82 +260,57 @@ def render_mm(self, code, options, _fmt, prefix="mermaid"):
mm_args = list(mermaid_cmd)

mm_args.extend(self.builder.config.mermaid_params)
mm_args += ['-i', tmpfn, '-o', outfn]
mm_args += ["-i", tmpfn, "-o", outfn]
if self.builder.config.mermaid_sequence_config:
mm_args.extend(["--configFile", self.builder.config.mermaid_sequence_config])

try:
p = Popen(
mm_args, shell=mermaid_cmd_shell, stdout=PIPE, stdin=PIPE, stderr=PIPE
)
p = Popen(mm_args, shell=mermaid_cmd_shell, stdout=PIPE, stdin=PIPE, stderr=PIPE)
except FileNotFoundError:
logger.warning(
"command %r cannot be run (needed for mermaid "
"output), check the mermaid_cmd setting" % mermaid_cmd
)
logger.warning("command %r cannot be run (needed for mermaid " "output), check the mermaid_cmd setting" % mermaid_cmd)
return None, None

stdout, stderr = p.communicate(str.encode(code))
if self.builder.config.mermaid_verbose:
logger.info(stdout)

if p.returncode != 0:
raise MermaidError(
"Mermaid exited with error:\n[stderr]\n%s\n"
"[stdout]\n%s" % (stderr, stdout)
)
raise MermaidError("Mermaid exited with error:\n[stderr]\n%s\n" "[stdout]\n%s" % (stderr, stdout))
if not os.path.isfile(outfn):
raise MermaidError(
"Mermaid did not produce an output file:\n[stderr]\n%s\n"
"[stdout]\n%s" % (stderr, stdout)
)
raise MermaidError("Mermaid did not produce an output file:\n[stderr]\n%s\n" "[stdout]\n%s" % (stderr, stdout))
return relfn, outfn


def _render_mm_html_raw(
self, node, code, options, prefix="mermaid", imgcls=None, alt=None
):
def _render_mm_html_raw(self, node, code, options, prefix="mermaid", imgcls=None, alt=None):
classes = ["mermaid"]
attrs = {}

if "align" in node:
classes.append(f"align-{node['align']}")
attrs["align"] = node["align"]

if "zoom_id" in node:
attrs["data-zoom-id"] = node["zoom_id"]

if "ids" in node and len(node["ids"]) == 1:
attrs["id"] = node["ids"][0]

tag_template = """<pre {attr_defs} class="{classes}">
{code}
</pre>"""
attr_defs = ["{}=\"{}\"".format(k, v) for k, v in attrs.items()]
self.body.append(
tag_template.format(
attr_defs=" ".join(attr_defs),
classes=" ".join(classes),
code=self.encode(code)
)
)
attr_defs = ['{}="{}"'.format(k, v) for k, v in attrs.items()]
self.body.append(tag_template.format(attr_defs=" ".join(attr_defs), classes=" ".join(classes), code=self.encode(code)))
raise nodes.SkipNode


def render_mm_html(self, node, code, options, prefix="mermaid", imgcls=None, alt=None):

_fmt = self.builder.config.mermaid_output_format
if _fmt == "raw":
return _render_mm_html_raw(
self, node, code, options, prefix="mermaid", imgcls=None, alt=None
)
return _render_mm_html_raw(self, node, code, options, prefix="mermaid", imgcls=None, alt=None)

try:
if _fmt not in ("png", "svg"):
raise MermaidError(
"mermaid_output_format must be one of 'raw', 'png', "
"'svg', but is %r" % _fmt
)
raise MermaidError("mermaid_output_format must be one of 'raw', 'png', " "'svg', but is %r" % _fmt)

fname, outfn = render_mm(self, code, options, _fmt, prefix)
except MermaidError as exc:
Expand All @@ -360,9 +330,7 @@ def render_mm_html(self, node, code, options, prefix="mermaid", imgcls=None, alt
self.body.append(svgtag)
else:
if "align" in node:
self.body.append(
'<pre align="%s" class="align-%s">' % (node["align"], node["align"])
)
self.body.append('<pre align="%s" class="align-%s">' % (node["align"], node["align"]))

self.body.append(f'<img src="{fname}" alt="{alt}" {imgcss}/>\n')
if "align" in node:
Expand All @@ -387,31 +355,21 @@ def render_mm_latex(self, node, code, options, prefix="mermaid"):
try:
p = Popen(mm_args, stdout=PIPE, stdin=PIPE, stderr=PIPE)
except OSError as err:
if err.errno != errno.ENOENT: # No such file or directory
if err.errno != errno.ENOENT: # No such file or directory
raise
logger.warning(
f"command {self.builder.config.mermaid_pdfcrop!r} cannot be run (needed to crop pdf), check the mermaid_cmd setting"
)
logger.warning(f"command {self.builder.config.mermaid_pdfcrop!r} cannot be run (needed to crop pdf), check the mermaid_cmd setting")
return None, None

stdout, stderr = p.communicate()
if self.builder.config.mermaid_verbose:
logger.info(stdout)

if p.returncode != 0:
raise MermaidError(
"PdfCrop exited with error:\n[stderr]\n%s\n"
"[stdout]\n%s" % (stderr, stdout)
)
raise MermaidError("PdfCrop exited with error:\n[stderr]\n%s\n" "[stdout]\n%s" % (stderr, stdout))
if not os.path.isfile(outfn):
raise MermaidError(
"PdfCrop did not produce an output file:\n[stderr]\n%s\n"
"[stdout]\n%s" % (stderr, stdout)
)
raise MermaidError("PdfCrop did not produce an output file:\n[stderr]\n%s\n" "[stdout]\n%s" % (stderr, stdout))

fname = "{filename[0]}-crop{filename[1]}".format(
filename=os.path.splitext(fname)
)
fname = "{filename[0]}-crop{filename[1]}".format(filename=os.path.splitext(fname))

is_inline = self.is_inline(node)
if is_inline:
Expand All @@ -428,9 +386,7 @@ def render_mm_latex(self, node, code, options, prefix="mermaid"):
elif node["align"] == "right":
self.body.append("{\\hspace*{\\fill}")
post = "}"
self.body.append(
"%s\\sphinxincludegraphics{%s}%s" % (para_separator, fname, para_separator)
)
self.body.append("%s\\sphinxincludegraphics{%s}%s" % (para_separator, fname, para_separator))
if post:
self.body.append(post)

Expand Down Expand Up @@ -492,15 +448,17 @@ def install_js(
_mermaid_js_url = f"https://cdn.jsdelivr.net/npm/mermaid@{app.config.mermaid_version}/dist/mermaid.esm.min.mjs"
elif app.config.mermaid_version:
raise MermaidError("Requires mermaid js version 10.3.0 or later")

app.add_js_file(_mermaid_js_url, priority=app.config.mermaid_js_priority, type="module")

if app.config.mermaid_elk_use_local:
_mermaid_elk_js_url = app.config.mermaid_elk_use_local
elif app.config.mermaid_include_elk == "latest":
_mermaid_elk_js_url = "https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs"
elif app.config.mermaid_include_elk:
_mermaid_elk_js_url = f"https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk@{app.config.mermaid_include_elk}/dist/mermaid-layout-elk.esm.min.mjs"
_mermaid_elk_js_url = (
f"https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk@{app.config.mermaid_include_elk}/dist/mermaid-layout-elk.esm.min.mjs"
)
else:
_mermaid_elk_js_url = None
if _mermaid_elk_js_url:
Expand All @@ -510,17 +468,15 @@ def install_js(
# Update if esm is used and no custom init-js is provided
if _mermaid_elk_js_url:
# Add registration of ELK layouts
app.config.mermaid_init_js = f'import mermaid from "{_mermaid_js_url}";import elkLayouts from "{_mermaid_elk_js_url}";mermaid.registerLayoutLoaders(elkLayouts);{app.config.mermaid_init_js}';
app.config.mermaid_init_js = f'import mermaid from "{_mermaid_js_url}";import elkLayouts from "{_mermaid_elk_js_url}";mermaid.registerLayoutLoaders(elkLayouts);{app.config.mermaid_init_js}'
else:
app.config.mermaid_init_js = f'import mermaid from "{_mermaid_js_url}";{app.config.mermaid_init_js}';
app.config.mermaid_init_js = f'import mermaid from "{_mermaid_js_url}";{app.config.mermaid_init_js}'

if app.config.mermaid_init_js:
# If mermaid is local the init-call must be placed after `html_js_files` which has a priority of 800.
priority = (
app.config.mermaid_init_js_priority if _mermaid_js_url is not None else 801
)
priority = app.config.mermaid_init_js_priority if _mermaid_js_url is not None else 801
app.add_js_file(None, body=app.config.mermaid_init_js, priority=priority, type="module")

_wrote_mermaid_run = False
if app.config.mermaid_output_format == "raw":
if app.config.d3_use_local:
Expand Down Expand Up @@ -553,7 +509,10 @@ def install_js(
_wrote_mermaid_run = True

if not _wrote_mermaid_run and _mermaid_js_url:
app.add_js_file(None, body=_MERMAID_RUN_NO_D3_ZOOM.format(mermaid_js_url=_mermaid_js_url), priority=app.config.mermaid_js_priority, type="module")
app.add_js_file(
None, body=_MERMAID_RUN_NO_D3_ZOOM.format(mermaid_js_url=_mermaid_js_url), priority=app.config.mermaid_js_priority, type="module"
)


def setup(app):
app.add_node(
Expand All @@ -574,7 +533,7 @@ def setup(app):
app.add_config_value("mermaid_params", list(), "html")
app.add_config_value("mermaid_verbose", False, "html")
app.add_config_value("mermaid_sequence_config", False, "html")

app.add_config_value("mermaid_use_local", "", "html")
app.add_config_value("mermaid_version", "11.2.0", "html")
app.add_config_value("mermaid_elk_use_local", "", "html")
Expand All @@ -587,4 +546,4 @@ def setup(app):
app.add_config_value("mermaid_d3_zoom", False, "html")
app.connect("html-page-context", install_js)

return {"version": sphinx.__display_version__, "parallel_read_safe": True}
return {"version": sphinx.__display_version__, "parallel_read_safe": True}
Loading