Coverage for src/midgy/python.py: 91%
92 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-24 15:45 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-24 15:45 -0700
1"""a minimal conversion from markdown to python code based on indented code blocks"""
3from dataclasses import dataclass, field
4from pathlib import Path
5from textwrap import dedent, indent
7from .render import DedentCodeBlock, escape
9__all__ = "Python", "md_to_python"
10SP, QUOTES = chr(32), ('"' * 3, "'" * 3)
12# the Python class translates markdown to python with the minimum number
13# of modifications necessary to have valid python code. midgy will:
14## add triple quotes to make python block strings of markdown blocks
15## escape quotes in markdown blocks
16## add indents to conform with python concepts
17# overall spaces, quotes, unicode escapes will be added to your markdown source.
18@dataclass
19class Python(DedentCodeBlock):
20 """a line-for-line markdown to python translator"""
22 include_docstring: bool = True
23 include_doctest: bool = False
24 include_front_matter: bool = True
25 include_markdown: bool = True
26 extend_continuations: bool = True
27 include_code_fences: list = field(default_factory=list)
29 front_matter_loader = '__import__("midgy").front_matter.load'
30 quote_char = chr(34)
32 def code_block(self, token, env):
33 if self.include_indented_code:
34 yield from super().code_block(token, env)
35 left = token.content.rstrip()
36 continued = left.endswith("\\")
37 if continued:
38 left = left[:-1]
39 env.update(
40 colon_block=left.endswith(":"),
41 quoted_block=left.endswith(QUOTES),
42 continued=continued,
43 )
45 def code_fence_block(self, token, env):
46 yield self.comment(self.get_block(env, token.map[0] + 1), env)
47 yield from self.get_block(env, token.map[1] - 1)
48 yield self.comment(self.get_block(env, token.map[1]), env)
49 env.update(colon_block=False, quoted_block=False, continued=False)
51 def comment(self, body, env):
52 return indent(dedent("".join(body)), SP * self._compute_indent(env) + "# ")
54 def doctest_comment(self, token, env):
55 yield from self.non_code(env, token)
56 yield (self.comment(self.get_block(env, token.map[1]), env),)
58 def doctest_code(self, token, env):
59 ref = env["min_indent"]
60 yield from self.non_code(env, token)
61 for line in self.get_block(env, token.meta["input"][1]):
62 right = line.lstrip()
63 yield line[ref : len(line) - len(right)] + right[4:]
64 if token.meta["output"]:
65 yield self.comment(self.get_block(env, token.meta["output"][1]), env)
66 env.update(colon_block=False, quoted_block=False, continued=False)
68 def fence_pycon(self, token, env):
69 if self.include_doctest:
70 yield from self.doctest_code(token, env)
71 elif self.include_docstring and self.include_markdown:
72 return
73 else:
74 yield from self.doctest_comment(token, env)
76 def format(self, body):
77 """blacken the python"""
78 from black import FileMode, format_str
80 return format_str(body, mode=FileMode())
82 def front_matter(self, token, env):
83 if self.include_front_matter:
84 trail = self.quote_char * 3
85 lead = f"locals().update({self.front_matter_loader}(" + trail
86 trail += "))"
87 body = self.get_block(env, token.map[1])
88 yield from self.wrap_lines(body, lead=lead, trail=trail)
89 else:
90 yield self.comment(self.get_block(env, token.map[1]), env)
92 def non_code(self, env, next=None):
93 if env.get("quoted_block", False):
94 yield from super().non_code(env, next)
95 elif self.include_markdown:
96 yield from self.non_code_block_string(env, next)
97 else:
98 yield from self.non_code_comment(env, next)
100 def non_code_block_string(self, env, next=None):
101 body = super().non_code(env, next)
102 trail = self.quote_char * 3
103 lead = SP * self._compute_indent(env) + trail
104 trail += "" if next else ";"
105 yield from self.wrap_lines(
106 map(escape, body),
107 lead=lead,
108 trail=trail,
109 continuation=self.extend_continuations and env.get("continued") and "\\" or "",
110 )
112 def non_code_comment(self, env, next=None):
113 yield self.comment(super().non_code(env, next), env)
115 def render_tokens(self, tokens, env=None, stop=None, src=None):
116 return dedent(super().render_tokens(tokens, env=env, stop=stop, src=src))
118 def shebang(self, token, env):
119 yield "".join(self.get_block(env, token.map[1]))
121 def _compute_indent(self, env):
122 """compute the indent for the first line of a non-code block."""
123 next = env.get("next_code")
124 next_indent = next.meta["first_indent"] if next else 0
125 spaces = prior_indent = env.get("last_indent", 0)
126 if env.get("colon_block", False): # inside a python block
127 if next_indent > prior_indent:
128 spaces = next_indent # prefer greater trailing indent
129 else:
130 spaces += 4 # add post colon default spaces.
131 return spaces - env.get("min_indent", 0)
134tangle = md_to_python = Python.code_from_string
137@dataclass
138class FencedPython(Python):
139 """a line-for-line markdown to python translator"""
141 include_code_fences: list = field(default_factory=["python", ""].copy)
144tangle = md_to_python = Python.code_from_string