"""The CommonMark document -- the object exported as ``cmx.doc``.
A ``CommonMark`` is an :class:`~cmx.backends.components.Article` (so it inherits
the component factories: ``table``, ``image``, ``figure``, ``row``, ``video``,
``savefig``) plus the live-document machinery: the ``@`` / ``|`` / call operators
for adding markdown, ``with doc:`` source capture, and ``flush`` to write out.
"""
import os
from copy import deepcopy
from contextlib import ExitStack
from . import components
from ..utils import get_block, dedent, SimpleLogger
from ..with_hack import SkipContextManager
# Re-exported so existing ``from cmx.backends.markdown import ...`` imports keep
# working after the components were unified into one module.
from .components import Image, Figure, Savefig, video # noqa: F401
USER = os.environ.get("USER", None)
PWD = os.environ.get("PWD", None)
def _green(message):
"""Return ``message`` wrapped in ANSI green (replaces the termcolor dep)."""
return f"\033[32m{message}\033[0m"
[docs]
class CommonMark(components.Article):
__filename = None
counter = 0
@property
def hide(self):
"""``with doc.hide:`` runs its body but does *not* capture the source as
a code block -- handy for boilerplate you want to execute but not show.
"""
return ExitStack()
@property
def skip(self):
"""``with doc.skip:`` skips execution of its body entirely.
Implemented via frame tracing (see :class:`SkipContextManager`); this can
interfere with debuggers such as PyCharm's pydev.
"""
return SkipContextManager(True)
def __init__(self, filename=None, overwrite=True, root=None, prefix=None, logger=None):
"""
:param filename: output markdown filename (auto-derived from the calling
script when omitted).
:param overwrite: clear the file on configure instead of appending.
:param logger: a pre-built logger (e.g. to a remote server); defaults to
a local :class:`SimpleLogger` rooted at ``root``/``prefix``.
"""
super().__init__()
self.logger = logger or SimpleLogger(root=root, prefix=prefix)
self.config(filename=filename, overwrite=overwrite)
[docs]
def config(self, filename=None, overwrite=True, src_prefix=None, logger=None):
self.overwrite = overwrite
self.logger = logger or self.logger
if filename:
self.__filename = filename
if self.overwrite:
self.logger.log_text("", filename=self.filename, overwrite=True)
if self.logger.root.startswith("http"):
print(_green("File output at " + self.logger.get_dash_url() + " " + self.__filename))
else:
from urllib import parse
print(_green("File output at file://" + parse.quote(os.path.realpath(self.__filename))))
return self
[docs]
def new(self, filename=None, **kwargs):
if filename:
filename = os.path.abspath(filename)
return deepcopy(self).config(filename=filename, **kwargs)
@property
def filename(self):
if self.__filename is None:
import inspect
# Walk out of cmx's own frames to find the user's script.
filename = "cmx/"
frame = inspect.currentframe()
while "cmx/" in filename or "importlib" in filename or "contextlib" in filename:
frame = frame.f_back
filename, line_number, function_name, lines, index = inspect.getframeinfo(frame)
if filename.endswith("__init__.py"):
self.__filename = filename[:-11] + "README.md"
else:
self.__filename = filename.replace(".py", ".md")
if self.overwrite:
self.clear()
from urllib import parse
print(_green("File output at file://" + parse.quote(self.__filename)))
return self.__filename
[docs]
def write(self, text, overwrite=None):
self.logger.log_text(text, filename=self.filename, overwrite=overwrite)
[docs]
def clear(self):
self.write("", overwrite=True)
def __call__(self, *snippets, dedent=True, **kwargs):
"""Add markdown text: ``doc("# Title")``."""
self.children.append(components.Text(*snippets, dedent=dedent, **kwargs))
return self
md = __call__
def __matmul__(self, string_or_array):
"""Prefix ``@`` syntax: ``doc @ "text"``."""
if isinstance(string_or_array, tuple):
string, *rest = string_or_array
return self(string, *rest)
return self(string_or_array)
def __ror__(self, string_or_array):
"""Postfix pipe syntax: ``"text" | doc``."""
if isinstance(string_or_array, tuple):
string, *rest = string_or_array
return self(string, *rest)
return self(string_or_array)
def __or__(self, other):
raise NotImplementedError("Left-side pipe operator not yet implemented")
[docs]
def pre(self, text, lang=None, **kwargs):
p = components.Pre(text, lang=lang, **kwargs)
self.children.append(p)
return p
[docs]
def yaml(self, data, **kwargs):
import yaml
return self.pre(yaml.dump(data).rstrip(), lang="yaml")
[docs]
def csv(self, csv, show_index=False, **kwargs):
t = components.Table(csv, show_index=show_index, logger=self.logger, **kwargs)
self.children.append(t)
return t
[docs]
def print(self, *args, sep=" ", end="\n"):
# Coalesce consecutive prints into a single code block, and echo to stdout.
if self.children and isinstance(self.children[-1], components.Print):
self.children[-1].text += sep.join([str(a) for a in args]) + end
else:
self.children.append(components.Print(*args, sep=sep, end=end))
print(*args, sep=sep, end=end)
[docs]
def flush(self, *args):
self.write(self._md)
self.children.clear()
def __enter__(self):
import inspect
assert self.filename, "make sure that file is already set."
prior_frame = inspect.currentframe().f_back
filename, line_number, function_name, lines, index = inspect.getframeinfo(prior_frame)
try:
lines_in_block = get_block(filename, line_number + 1)
text = "".join(lines_in_block)
self.children.append(components.Pre(dedent(text).rstrip(), lang="python"))
except FileNotFoundError:
print("in iPython session")
return self
__exit__ = flush
[docs]
class Github(CommonMark):
"""Uses tables for the layout."""
pass
[docs]
class Gist(CommonMark):
"""Saves everything inside a folder."""
pass