from pathlib import Path
from astropy.table import TableFormatter
from docutils.core import publish_doctree, publish_parts
from docutils.nodes import comment, literal_block
import yaml
from .. import rc
from ..utils import from_currsys
[docs]def walk(node, context_code=None):
"""
Recursively walk through a docutils doctree and run/plot code blocks
Parameters
----------
node : docutils.node.Node
context_code : str, optional
A code string inherited from previous code/comment nodes
Returns
-------
context_code : str
Code to be inherited by subsequent code/comment nodes
"""
if context_code is None:
context_code = """
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
"""
if isinstance(node, (comment, literal_block)):
if isinstance(node, comment):
context_code = process_comment_code(node, context_code)
elif isinstance(node, literal_block):
context_code = process_literal_code(node, context_code)
exec(context_code)
elif hasattr(node, "children"):
for child in node.children:
context_code = walk(child, context_code)
return context_code
[docs]def process_literal_code(node, context_code):
"""Add code from a ``literal_block`` node to the context_code string"""
new_code = node.rawsource
attribs = node.attributes["classes"]
action = [att for att in attribs if "format-" not in att]
format = [att.replace("format-", "") for att in attribs if "format-" in att]
format = format if len(format) > 0 else ["png"]
options = {"action": action,
"format": format}
if len(node.attributes["names"]) > 0:
options["name"] = node.attributes["names"][0]
context_code = process_code(context_code, new_code, options)
return context_code
[docs]def process_code(context_code, code, options):
"""
Extracts and adds code from the node text to the context_code string
Code can be passed in either a ``literal_block`` or ``comment`` docutils
node. The options regarding what to do with the code are included in either
the :class: and :name: tag of a ``literal_block`` node, or in a yaml header
section in a ``comment`` node.
See below for examples of code blocks.
Options for controlling what happens to the code block are as follows:
- name: The filename for the plot
- format: Any matplotlib-accepted file format, e.g: png, pdf, svg, etc.
Multiple file formats can be
- action: [reset, clear-figure, plot]
- ``reset`` clears the context_code string. By default code is saved
from previous code blocks.
- ``clear-figure`` adds ``plt.clf()`` to the context string before the
current code block is added
- ``plot`` adds ``plt.savefig({name}.{format})`` to the context string.
If multiple formats are given, these will be iterated over.
For ``comment`` blocks, options should be given in a yaml style header,
separated form the code block by exactly three (3) hyphens ('---')
For ``literal_block`` blocks, we hijack the ``:class:`` and ``:name:``
attributes. See the examples below. All action keywords are passed to
``:class:``. Format keys are passed as ``format-<key>``.
Parameters
----------
context_code : str
code from previous Nodes
code : str
code from the current Node
options : dict
options dictionary derived from Node attributes or RST header blocks
Returns
-------
context_code : str
Examples
--------
Example of ``literal_block`` code block::
.. code::
:name: my_fug
:class: reset, clear-figure, plot, format-png
plt.plot([0,1], [1,1])
.. figure:: my_fug.png
:name: fig:my_fug
Example of a ``comment`` code block::
..
name: my_fug2
format: [jpg, svg]
action: [reset, clear-figure, plot]
---
plt.plot([0,1], [1,0])
.. figure:: my_fug2.jpg
:name: fig:my_fug2
"""
if "reset" in options.get("action", []):
context_code = ""
if "clear-figure" in options.get("action", []):
context_code += "\nimport matplotlib.pyplot as plt\nplt.clf()\n"
if "execute" in options.get("action", []):
context_code += code
if "plot" in options.get("action", []):
context_code += code
img_path = options.get("path", rc.__config__["!SIM.reports.image_path"])
formats = options.get("format", ["png"])
formats = [formats] if isinstance(formats, str) else formats
for fmt in formats:
fname = options.get("name", "untitled").split(".")[0]
fname = ".".join([fname, fmt])
fname = Path(img_path, fname)
# This commented out code will not work in windows in the future:
# context_code += f"\nplt.savefig(\"{fname}\")"
# Because on windows it results in context_code like
# plt.savefig("images_temp\my_fug3.png")
# which has a '\m' in it, which is an invalid escape sequence,
# which will be a SyntaxError in the future.
# (The code probably already creates figures with incorrect paths
# if the name of the file would lead to a valid escape sequence
# like \t or \n.)
# Therefor it is necessary to first convert fname to a string, and
# then use `repr` on that to (on windows) escape the slashes. repr
# also adds its own (single) quotes, so it is not necessary to
# include them in the context_code.
context_code += f"\nplt.savefig({repr(str(fname))})"
return context_code
[docs]def plotify_rst_text(rst_text):
"""
Generates and saves plots from code blocks in an RST string
The save directory for the plots defaults to
``scopesim.rc.__config__["!SIM.reports.image_path"]``.
This can be overridden ONLY inside a COMMENT block using the path keyword.
Parameters
----------
rst_text : str
Any RST text string
Examples
--------
The following rst text will generate a plot and save it in three formats::
A basic plot
============
Let's make a basic plot using the comment style
..
action: plot
format: [pdf, png]
name: my_fug
path: "./images/"
---
plt.plot([0,1], [0,1])
And now a plot using the code block style
.. code::
:name: my_fug3
:class: reset, plot, format-jpg, format-svg
import matplotlib.pyplot as plt
plt.plot([0,1], [1,1])
The plot are created be calling::
plotify_rst_text(rst_text)
where ``rst_text`` is a string holding the full RST text from above.
Notes
-----
* Possible actions are: [reset, clear-figure, plot]
* Code is retained between code blocks in the same string, so we do not need
to re-write large sections of code
* By default ``import numpy as np`` and ``import matplotlib.pyplot as plt``
are loaded automatically, so these do not need to be explicitly specified
in each code block.
* THE EXCEPTION is when the action ``reset`` is specified. This clears the
code_context variable.
"""
walk(publish_doctree(rst_text))
[docs]def latexify_rst_text(rst_text, filename=None, path=None, title_char="=",
float_figures=True, use_code_box=True):
"""
Converts an RST string (block of text) into a LaTeX string
NOTE: plots will NOT be generated with this command. For that we must invoke
the ``plotfiy`` command.
Parameters
----------
rst_text : str
filename : str, optional
What to name the latex file. If None, the filename is derived from the
text title
path : str, optional
Where to save the latex file
title_char : str, optional
The character used to underline the rst text title. Usually "=".
float_figures : bool, optional
Set to False if figures should not be placed by LaTeX.
Replaces all ``\begin{figure}`` with ``\begin{figure}[H]``
use_code_box : bool, optional
Adds a box around quote blocks
Returns
-------
tex_str : str
The same string or block of text in LaTeX format
Examples
--------
::
rst_text = '''
Meaning of life
===============
Apparently it's 42. Lets plot
..
action: plot
name: my_plot
path: ./images/
---
plt.plot([0,1], [0,1])
.. figure:: images/my_plot.png
:name: label-my-plot
This is an included figure caption
'''
plotify_rst_text(rst_text)
latexify_rst_text(rst_text, filename="my_latex_file", path="./")
"""
if path is None:
path = from_currsys(rc.__config__["!SIM.reports.latex_path"])
if filename is None:
filename = rst_text.split(title_char)[0].strip().replace(" ", "_")
text = "Title\n<<<<<\nSubtitle\n>>>>>>>>\n\n"
parts = publish_parts(
text + rst_text,
writer_name="latex",
# Settings_overrides to placate FutureWarnings.
# TODO: Decide whether the future defaults look better.
settings_overrides={
"use_latex_citations": False,
"legacy_column_widths": True,
},
)
if not float_figures:
parts["body"] = parts["body"].replace("begin{figure}",
"begin{figure}[H]")
if use_code_box:
parts["body"] = parts["body"].replace("begin{alltt}",
"begin{alltt}\n\\begin{lstlisting}[frame=single]")
parts["body"] = parts["body"].replace("end{alltt}",
"end{lstlisting}\n\\end{alltt}")
file_path = Path(path, filename).with_suffix(".tex")
with open(file_path, "w") as f:
f.write(parts["body"])
tex_str = parts["body"]
return tex_str
[docs]def rstify_rst_text(rst_text, filename=None, path=None, title_char="="):
""" The same as ``latexify_rst_text```, but the output is in RST format """
if path is None:
path = from_currsys(rc.__config__["!SIM.reports.rst_path"])
if filename is None:
filename = rst_text.split(title_char)[0].strip().replace(" ", "_")
file_path = Path(path, filename).with_suffix(".rst")
with open(file_path, "w") as f:
f.write(rst_text)
return rst_text
[docs]def table_to_rst(tbl, indent=0, rounding=None):
if isinstance(rounding, int):
for col in tbl.itercols():
if col.info.dtype.kind == "f":
col.info.format = f".{rounding}f"
tbl_fmtr = TableFormatter()
lines, outs = tbl_fmtr._pformat_table(tbl, max_width=-1, max_lines=-1,
show_unit=False)
i = outs["n_header"] - 1
lines[i] = lines[i].replace("-", "=")
lines = [lines[i]] + lines + [lines[i]]
indent = " " * indent
rst_str = indent + ("\n" + indent).join(lines)
return rst_str