|
1 | 1 | import inspect |
2 | 2 | import pathlib |
| 3 | +import subprocess |
3 | 4 | import textwrap |
4 | 5 |
|
5 | 6 |
|
| 7 | +_executors = {} |
| 8 | + |
| 9 | + |
| 10 | +def register_executor(lang, executor): |
| 11 | + """Add a new executor for markdown code blocks |
| 12 | +
|
| 13 | + lang should be the tag used after the opening ``` |
| 14 | + executor should be a callable that takes one argument: |
| 15 | + the code block found |
| 16 | + """ |
| 17 | + _executors[lang] = executor |
| 18 | + |
| 19 | + |
| 20 | +def exec_bash(source): |
| 21 | + """Exec the bash source given in a new subshell |
| 22 | +
|
| 23 | + Does not return anything, but if any command returns not-0 an error |
| 24 | + will be raised |
| 25 | + """ |
| 26 | + command = ["bash", "-e", "-u", "-c", source] |
| 27 | + try: |
| 28 | + subprocess.run(command, check=True) |
| 29 | + except Exception: |
| 30 | + print(source) |
| 31 | + raise |
| 32 | + |
| 33 | + |
| 34 | +register_executor("bash", exec_bash) |
| 35 | + |
| 36 | + |
| 37 | +def exec_python(source): |
| 38 | + """Exec the python source given in a new module namespace |
| 39 | +
|
| 40 | + Does not return anything, but exceptions raised by the source |
| 41 | + will propagate out unmodified |
| 42 | + """ |
| 43 | + try: |
| 44 | + exec(source, {"__MODULE__": "__main__"}) |
| 45 | + except Exception: |
| 46 | + print(source) |
| 47 | + raise |
| 48 | + |
| 49 | + |
| 50 | +register_executor("", exec_python) |
| 51 | +register_executor("python", exec_python) |
| 52 | + |
| 53 | + |
6 | 54 | def get_codeblock_members(*classes): |
7 | 55 | """ |
8 | 56 | Grabs the docstrings of any methods of any classes that are passed in. |
@@ -61,49 +109,54 @@ def check_docstring(obj, lang=""): |
61 | 109 | """ |
62 | 110 | Given a function, test the contents of the docstring. |
63 | 111 | """ |
| 112 | + if lang not in _executors: |
| 113 | + raise LookupError( |
| 114 | + f"{lang} is not a supported language to check\n" |
| 115 | + "\tHint: you can add support for any language by using register_executor" |
| 116 | + ) |
| 117 | + executor = _executors[lang] |
64 | 118 | for b in grab_code_blocks(obj.__doc__, lang=lang): |
65 | | - try: |
66 | | - exec(b, {"__MODULE__": "__main__"}) |
67 | | - except Exception: |
68 | | - print(f"Error Encountered in `{obj.__name__}`. Caused by:\n") |
69 | | - print(b) |
70 | | - raise |
| 119 | + executor(b) |
71 | 120 |
|
72 | 121 |
|
73 | 122 | def check_raw_string(raw, lang="python"): |
74 | 123 | """ |
75 | 124 | Given a raw string, test the contents. |
76 | 125 | """ |
| 126 | + if lang not in _executors: |
| 127 | + raise LookupError( |
| 128 | + f"{lang} is not a supported language to check\n" |
| 129 | + "\tHint: you can add support for any language by using register_executor" |
| 130 | + ) |
| 131 | + executor = _executors[lang] |
77 | 132 | for b in grab_code_blocks(raw, lang=lang): |
78 | | - try: |
79 | | - exec(b, {"__MODULE__": "__main__"}) |
80 | | - except Exception: |
81 | | - print(b) |
82 | | - raise |
| 133 | + executor(b) |
83 | 134 |
|
84 | 135 |
|
85 | 136 | def check_raw_file_full(raw, lang="python"): |
| 137 | + if lang not in _executors: |
| 138 | + raise LookupError( |
| 139 | + f"{lang} is not a supported language to check\n" |
| 140 | + "\tHint: you can add support for any language by using register_executor" |
| 141 | + ) |
| 142 | + executor = _executors[lang] |
86 | 143 | all_code = "" |
87 | 144 | for b in grab_code_blocks(raw, lang=lang): |
88 | 145 | all_code = f"{all_code}\n{b}" |
89 | | - try: |
90 | | - exec(all_code, {"__MODULE__": "__main__"}) |
91 | | - except Exception: |
92 | | - print(all_code) |
93 | | - raise |
94 | | - |
| 146 | + executor(all_code) |
| 147 | + |
95 | 148 |
|
96 | | -def check_md_file(fpath, memory=False): |
| 149 | +def check_md_file(fpath, memory=False, lang="python"): |
97 | 150 | """ |
98 | 151 | Given a markdown file, parse the contents for python code blocks |
99 | | - and check that each independant block does not cause an error. |
| 152 | + and check that each independent block does not cause an error. |
100 | 153 |
|
101 | 154 | Arguments: |
102 | 155 | fpath: path to markdown file |
103 | | - memory: wheather or not previous code-blocks should be remembered |
| 156 | + memory: whether or not previous code-blocks should be remembered |
104 | 157 | """ |
105 | 158 | text = pathlib.Path(fpath).read_text() |
106 | 159 | if not memory: |
107 | | - check_raw_string(text, lang="python") |
| 160 | + check_raw_string(text, lang=lang) |
108 | 161 | else: |
109 | | - check_raw_file_full(text, lang="python") |
| 162 | + check_raw_file_full(text, lang=lang) |
0 commit comments