"""
Functions and sources for the markdown tutorial slides!
"""
import inspect
import re
from collections import OrderedDict
from typing import Callable, List, Optional, Union
import yaml
import lookatme
import lookatme.config as config
import lookatme.utils as utils
[docs]class Tutor:
"""A class to handle/process tutorials for specific functionality
In addition to name, group, and slides content of the tutor, each Tutor
must also be associated with the implementation.
"""
def __init__(
self,
name: str,
group: str,
slides_md: str,
impl_fn: Callable,
order: int,
lazy_formatting: Optional[Callable] = None,
):
"""Create a new Tutor
Args:
lazy_formatting. Callable. Should return a dictionary that will be
unpacked into the kwargs of str.format()
"""
self.name = name
self.group = group
self.slides_md = inspect.cleandoc(slides_md).strip()
self.impl_fn = impl_fn
self.order = order
self.lazy_formatting = lazy_formatting
[docs] def get_md(self, rendered_example=True) -> str:
"""Get the tutor's markdown text after resolving any special markup
contained in it.
opts. Dict[str, Any]
slides. Current can include `{
"""
slides_md = self.slides_md
if self.lazy_formatting is not None:
slides_md = slides_md.format(**self.lazy_formatting())
tag_handlers = {
"EXAMPLE": lambda contents: self._handle_show_and_render(contents, rendered_example),
"STYLE": self._handle_style_yaml,
}
res_md = []
last_idx = 0
regex = "<(?P<tag>TUTOR:(?P<type>[A-Z_]+))>(?P<inner>.*)</(?P=tag)>"
for match in re.finditer(regex, slides_md, re.MULTILINE | re.DOTALL):
res_md.append(slides_md[last_idx: match.start()])
match_groups = match.groupdict()
handler = tag_handlers.get(match_groups["type"], None)
if handler is None:
raise ValueError(
"No handler defined for 'TUTOR:{}' tags".format(
match_groups["type"]
)
)
res_md.append(handler(match_groups["inner"]))
last_idx = match.end() + 1
res_md.append(slides_md[last_idx:])
return "\n\n".join([
self._get_heading(),
"".join(res_md),
self._get_source_note(),
])
def _get_heading(self) -> str:
return "# {group}: {name}".format(
group=self.group.title(),
name=self.name.title(),
)
def _get_source_note(self) -> str:
link = self._get_source_link()
return "> This is implemented in {}".format(link)
def _get_source_link(self):
file_name = inspect.getsourcefile(inspect.unwrap(self.impl_fn))
if file_name is None:
return "??"
relpath = file_name.split("lookatme/", 1)[1]
_, lineno = inspect.getsourcelines(self.impl_fn)
version = "v" + lookatme.VERSION
return "[{module}.{fn_name}]({link})".format(
module=self.impl_fn.__module__,
fn_name=self.impl_fn.__qualname__,
link="https://github.com/d0c-s4vage/lookatme/blob/{version}/{path}#L{lineno}".format(
version=version,
path=relpath,
lineno=lineno,
)
)
def _handle_show_and_render(self, contents, rendered_example: bool = True) -> str:
contents = contents.strip()
markdown_example = "\n".join([
"~~~markdown",
contents,
"~~~",
])
quoted_example = utils.prefix_text(markdown_example, "> ")
res = [
"***Markdown***:",
quoted_example,
]
if rendered_example:
res += [
"***Rendered***:",
contents + "\n",
]
return "\n\n".join(res)
def _handle_style_yaml(self, contents: str) -> str:
contents = contents.strip()
style = config.get_style()[contents]
style = {"styles": {contents: style}}
return "```yaml\n---\n{style_yaml}---\n```".format(
style_yaml=yaml.dump(style).encode().decode("unicode-escape"),
)
GROUPED_TUTORIALS = OrderedDict()
NAMED_TUTORIALS = OrderedDict()
[docs]def get_tutorial_help() -> str:
res = []
res.append(inspect.cleandoc("""
Help for 'lookatme --tutorial'
Specific tutorials can be run with a comma-separated list of group or
tutorial names. Below are the groups and tutorial names currently defined.
""").strip())
for group_name, group_tutors in GROUPED_TUTORIALS.items():
res.append("")
res.append(" " + group_name)
for tutor_name in group_tutors.keys():
res.append(" " + tutor_name)
res.append("")
res.append(inspect.cleandoc("""
Substring matching is used to identify tutorials and groups. All matching
tutorials and groups are then run.
Examples:
lookatme --tutorial
lookatme --tutorial link,table
lookatme --tutorial general,list
""").strip())
return "\n".join(res)
[docs]def print_tutorial_help():
print(get_tutorial_help())
[docs]def tutor(
group: str,
name: str,
slides_md: str,
order: int = 99999,
lazy_formatting: Optional[Callable] = None
):
"""Define tutorial slides by using this as a decorator on a function!"""
def capture_fn(fn):
tutor = Tutor(name, group, slides_md, fn, order, lazy_formatting)
tutor_list = (
GROUPED_TUTORIALS
.setdefault(group, OrderedDict())
.setdefault(name, [])
)
tutor_list.append(tutor)
NAMED_TUTORIALS.setdefault(name, []).append(tutor)
return fn
return capture_fn
[docs]def pretty_close_match(str1, str2):
str1 = str1.lower()
str2 = str2.lower()
if str1 in str2 or str2 in str1:
return True
[docs]@tutor(
"general",
"tutorial",
r"""
Lookatme has a built-in tutorial feature that can be used for reference.
To launch lookatme's tutorial slides, run lookatme with the `--tutorial`
argument:
```
lookatme --tutorial
```
## Specific Tutorials
Tutorials each have individual names and are organized into groups. You can
pass comma-separated values of strings that ~match group or tutorial names
to only run those tutorial slides:
```bash
# only run the general slide group, and the list-related tutorials
lookatme --tutorial general,list
# only run the table tutorial slides
lookatme --tutorial table
```
## Seeing What's Available
If you pass the `help` value to `--tutorial`, lookatme me will list all
groups defined and all tutorial names within those groups. Currently this
is the output of `lookatme --tutorial help`. Notice that tutorial names
are nested under the group names:
```
{lookatme_help}
```
""",
order=99, # last
lazy_formatting=lambda: {
"lookatme_help": get_tutorial_help()
}
)
def get_tutors(group_or_tutor: str) -> List[Tutor]:
for group_name, group_value in GROUPED_TUTORIALS.items():
if pretty_close_match(group_name, group_or_tutor):
return list(group_value.values())
res = []
for grouped_tutors in GROUPED_TUTORIALS.values():
for tutor_name, tutor_list in grouped_tutors.items():
if pretty_close_match(tutor_name, group_or_tutor):
res.append(tutor_list)
return res
def _sort_tutors_by_order():
for group_name, tutors in list(GROUPED_TUTORIALS.items()):
del GROUPED_TUTORIALS[group_name]
tutor_list = list(tutors.items())
tutor_list = list(sorted(
tutor_list,
key=lambda x: min(tutor.order for tutor in x[1]),
))
GROUPED_TUTORIALS[group_name] = OrderedDict(tutor_list)
[docs]def get_tutorial_md(groups_or_tutors: List[str]) -> Union[None, str]:
_sort_tutors_by_order()
tutors = []
for group_or_tutor in groups_or_tutors:
tutors += get_tutors(group_or_tutor)
if len(tutors) == 0:
return None
res_slides = []
for tutor in tutors:
tutor_md = "\n\n".join(t.get_md() for t in tutor)
res_slides.append(tutor_md)
meta = inspect.cleandoc("""
---
title: lookatme Tutorial
author: lookatme devs
---
""").strip()
return meta + "\n" + "\n\n---\n\n".join(res_slides) + "\n\n---\n\n# End"