Skip to content

Instantly share code, notes, and snippets.

@mondeja
Last active November 30, 2020 06:54
Show Gist options
  • Save mondeja/084585268fec1c71515c69119b2d204f to your computer and use it in GitHub Desktop.
Save mondeja/084585268fec1c71515c69119b2d204f to your computer and use it in GitHub Desktop.
Python-to-Markdown magics line table generator
# DataFrame example
df = pd.DataFrame(data={'col1': ["one", 2], 'col2': [3, "four"]})
df = df.to_dict() # You need to pass as dict
%md table $df # <---- In Jupyter Notebook cell
#------------------------------------------------------------
# Dict structure example
dstruct = dict(columns=["col1", "col2"], values=[["first", 2], [3, "four"]])
# Can be called as markdown or md
%markdown table $dstruct -v -i
from re import findall, search
from ast import literal_eval
from IPython.core.magic import Magics, magics_class, line_magic
from IPython.display import display_javascript
@magics_class
class MarkdownMagics(Magics):
"""
Markdown tables cell code generator from Python data structures.
Args:
replace (bool, optional): If True, the caller cell will be replaced
with the call command. You can pass this flag in individual
commands with ``-r``. As default, ``False``.
inplace (bool, optional): If True, the code generated will replace
the code in the cell generated, so use this flag with caution
as ``-i``. As default, ``False``.
verbose (bool, optional): If True, the command rendered will be showed
in the next cell of caller (if ``replace == True`` the caller
cell will be replaced instead. Pass this flag to individual commands
with ``-v``. As default, ``False``.
"""
def __init__(self, shell, replace=False, inplace=False, verbose=False):
super(MarkdownMagics, self).__init__(shell)
self.replace = replace # Replace cell on ouput
self.inplace = inplace # Inplace code on ouput cell (False will add code to the end of the line)
self.verbose = verbose # Create a new code cell with the call rendered
# Original arguments
self._replace = replace
self._inplace = inplace
self._verbose = verbose
def render_cell_in_markdown_format(self, content):
jscode = "var t_cell = IPython.notebook.get_selected_cell();"
if self.inplace:
jscode += "t_cell.set_text('{}');".format(content.replace('\n','\\n'))
else:
jscode += "t_cell.set_text(t_cell.get_text() + '\\n' + '{}');".format(content.replace('\n','\\n'))
jscode += """var t_index = IPython.notebook.get_cells().indexOf(t_cell);
IPython.notebook.to_markdown(t_index);
IPython.notebook.get_cell(t_index).render();"""
return display_javascript(jscode, raw=True)
def command_parser(self, line=""):
# Parse global flags
map_global_flags = {"-i": "inplace", "-r": "replace", "-v": "verbose"}
for flag, attr in map_global_flags.items():
if " %s" % flag in line:
exec("self.%s = True" % attr)
line.replace(" %s" % flag, "")
else:
exec("self.%s = self._%s" % (attr, attr))
# Get magics function name and optional arguments
if " " in line: # With optional arguments
_splitted = line.split(" ")
funcname = _splitted[0]
args = " ".join(_splitted[1:])
else: # Without optional arguments
args = ""
funcname = line
return funcname, args
#--------------------------------------------------------
# Markdown magic line functions
#--------------------------------------------------------
@line_magic
def md(self, line):
return self.markdown(line, called_as_md=True)
@line_magic
def markdown(self, line, called_as_md=False):
if called_as_md:
return eval('self._%s("%s", called_as_md=True)' % self.command_parser(line))
return eval('self._%s("%s")' % self.command_parser(line))
def _table(self, line="", called_as_md=False):
"""
Generates a table in the next markdown cell.
Can be called with the commands:
- %markdown table
- %md table $data
- %md table center=True
Args:
rows (int, optional): NUmber of rows of the table. Only
works if ``data == None``. As default, ``3``.
cols (int, optional): NUmber of cols of the table. Only
works if ``data == None``. As default, ``3``.
data (dict, optional): Python variables that will be rendered in
a table if is possible. You can pass a ``pandas.DataFrame``
(needs to be converted to dict with ``df.to_dict()`` method),
or a dictionary with the following fields and structure:
``dict(columns=["col1", "col2"],
values=[["first", 2], [3, "four"]])``
As default, ``None`` (a void table will be created).
center (bool, optional): Header table separator will be build
with center marks like ``:----:``. As default, ``False``.
header (bool, optional): Select if you want to include a header.
As default, ``False``.
separator (bool, optional): Select if you want to include a
separator. As default, ``False``.
body (bool, optional): Select if you want to include table body.
as default, ``False``.
"""
args = dict(rows=3, cols=3, data=None,
center=False, header=True,
separator=True, body=True)
if line != "":
__args = line.split()
if __args[0].isnumeric():
args["rows"] = int(__args[0])
if __args[1].isnumeric():
args["cols"] = int(__args[1])
data = search(r'{.+}', line)
if data:
args["data"] = literal_eval(data.group(0))
# Check if data is formatted as pd.DataFrame.to_dict()
# or {"colums": <list>, "values": <list of lists>}
PANDAS_DF = False
new_dict = {"columns": [], "values": []}
for key, value in args["data"].items():
if isinstance(value, dict): # As pd.DataFrame
PANDAS_DF = True
elif isinstance(value, list):
break
if PANDAS_DF:
new_dict["columns"].append(key)
new_dict["values"].append(list(value.values()))
if PANDAS_DF:
args["data"] = new_dict
# Optional arguments parsing
kwargs = findall(r"\s*([a-z]+)=([TrueFals]+)\s*", line)
for arg, value in kwargs:
args[arg] = True if value == "True" else False
if args["data"]:
max_len = max([
max([len(str(value)) for value in args["data"]["columns"]]),
max([len(str(value)) for row in args["data"]["values"] for value in row])
])
def _fill_celd(max_len, value):
space = max([0, max_len - len(str(value))])
if space % 2 == 0:
spaces_left = spaces_right = int(space/2)
else:
spaces_left = int((space/2)+1)
spaces_right = int(space/2)
return " %s%s%s " % (" "*spaces_left, str(value), " "*spaces_right)
response = ""
if args["header"] or args["data"]:
args["cols"] -= 1
header = ""
if args["data"]:
for i in args["data"]["columns"]:
header += "|%s" % _fill_celd(max_len, i)
else:
for _ in range(args["rows"]):
header += "| "
header += "|\n"
response += header
if args["separator"] or args["data"]:
args["cols"] -= 1
separator = ""
_range = len(args["data"]["columns"]) if args["data"] else args["rows"]
for _ in range(_range):
separator += "|"
if args["center"]:
separator += ":-"
if args["data"]:
separator += "-"*(max_len+2) if not args["center"] else "-"*(max_len-2)
else:
if not args["centered"]:
separator += "----"
if args["center"]:
separator += "-:"
separator += "|\n"
response += separator
if args["body"] or args["data"]:
body = ""
if args["data"]:
for row in args["data"]["values"]:
_row = ""
for value in row:
_row += "|%s" % _fill_celd(max_len, value)
_row += "|\n"
body += _row
response += body
else:
for c in range(args["cols"]):
_row = ""
for r in range(args["rows"]):
_row += "| "
_row += "|\n"
body += _row
response += body
how_was_called = "markdown" if not called_as_md else "md"
if self.verbose or self.replace:
self.shell.set_next_input("%%%s table %s\n" % (how_was_called, line), replace=self.replace)
return self.render_cell_in_markdown_format(response)
ip = get_ipython()
magics = MarkdownMagics(ip)
ip.register_magics(magics)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment