├── .gitignore ├── PyLiterate.sublime-build ├── README.md ├── example ├── Chapter1 │ ├── Falafel_balls.md │ ├── Introduction.md │ └── Onion_rings.md ├── Chapter2 │ ├── Introduction.md │ ├── Salmon_fillet.md │ ├── Swordfish_steaks.md │ └── my_include.py └── Makefile ├── pyliterate ├── __init__.py └── run_markdown.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | lib 3 | pyvenv.cfg 4 | *.egg-info 5 | example/output 6 | *__pycache__* 7 | -------------------------------------------------------------------------------- /PyLiterate.sublime-build: -------------------------------------------------------------------------------- 1 | { 2 | "cmd": ["run_markdown", "--overwrite", "$file"], 3 | "selector": "text.html.markdown.gfm", 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyLiterate 2 | 3 | This is the tool I used to write my book, [Effective Python](http://www.effectivepython.com). The workflow is a variation on [Donald Knuth's Literate programming](http://en.wikipedia.org/wiki/Literate_programming). 4 | 5 | The idea is the source code and explanatory text appear interleaved in a [Markdown formatted](https://help.github.com/articles/github-flavored-markdown/) file. Each time you edit a Markdown file, you can re-run the source code it contains using the `run_markdown` tool provided by this project (it's super userful as a Sublime build system). `run_markdown` will update the file in-place with the new output from running the code. If an unexpected error occurs it will be printed to the terminal instead of overwriting the file. 6 | 7 | The example book source contained in this project has multiple chapters (directories) containing multiple subsections (files). By having these subsections in separate files, it becomes a lot easier to focus on each individual subject as you write. The example `Makefile` also shows how all of the pieces can be connected together. 8 | 9 | In the case of my book, the final step was post-processing the Markdown file and converting it into a Microsoft Word document. This was a requirement of my publisher because of the way their book printing workflow was built. You can [learn more about how I wrote my book here](http://www.onebigfluke.com/2014/07/how-im-writing-programming-book.html). 10 | 11 | ## Example usage 12 | 13 | See the `example` directory for the layout of a simple book. This includes an example `Makefile` that ties it all together. To run the example, run the following commands from a clean check-out of this project. Make sure you have Python 3 installed. 14 | 15 | Create a virtual environment: 16 | 17 | `pyvenv .` 18 | 19 | Activate the virtual environment: 20 | 21 | `source bin/activate` 22 | 23 | Install this package in your virtual environment: 24 | 25 | `pip install -e .` 26 | 27 | Go into the example directory: 28 | 29 | `cd example` 30 | 31 | Run all of the Markdown files and have them overwritten in place: 32 | 33 | `make run` 34 | 35 | Build the whole book output: 36 | 37 | `make output/Book_draft.md` 38 | 39 | Making the book will also output diffs of each section after the source is re-run. This allows you to see if running the latest version of the code in the Markdown files changes the output in any way. 40 | 41 | When you're done with everything, you can delete the output files: 42 | 43 | `make clean` 44 | 45 | And finally, deactivate your virtual environment: 46 | 47 | `deactivate` 48 | 49 | ## Markdown format 50 | 51 | The `run_markdown` tool looks for code blocks like this in Markdown files (that end with the `.md` suffix): 52 | 53 | ```python 54 | print('Hello world') 55 | ``` 56 | 57 | The tool will run the code top-to-bottom in the file. When a non-Python block like this is found (it also can be empty): 58 | 59 | ``` 60 | Output goes here 61 | ``` 62 | 63 | All of the prior Python blocks are combined together, run, and their combined output is inserted (`Hello world` in this case). Not every Python block needs to have an output block, but every output block needs at least one preceeding Python block to produce output. 64 | 65 | Python blocks do not have to be stand-alone code blocks. A single class or function definition can be interleaved with text. For example, this is legal: 66 | 67 | ```python 68 | def multiply(a, b): 69 | ``` 70 | 71 | And the body is: 72 | 73 | ```python 74 | return a * b 75 | ``` 76 | 77 | Here's a list of various detailed features that are provided by the `run_markdown` tool: 78 | 79 | - `pprint` function is always available without import 80 | - `debug` function will write output to stderr but not insert it into the Markdown file's output 81 | - `Pdb.set_trace()` will actually stop execution in the Markdown file so you can debug as the program runs 82 | - Exception tracebacks and PDB step line numbers are all given as line numbers in the original Markdown file 83 | - Use ```````python```` to run a code snippet as Python 3 source. This will automatically inherit all of the state of the program for any snippets that are higher up in the Markdown file 84 | - Use ```````python2```` to run a code snippet as Python 2 source instead of Python 3. Notably, Python 2 snippets will not inherit the Python execution state from higher up in the file, they are limited to the containing ``` block 85 | - Use ```````python-exception```` for expected exceptions for which you want to insert the exception name and error message back into the Markdown file 86 | - Use ```````python-syntax-error```` for examples that contain syntax errors that you want to insert back into the Markdown file 87 | - Use ```````python-include:path/to/file.py```` to include an external Python file relative to the `--root_dir` flag, which defaults to the root of the book directory. This will automatically insert a comment of the source file's relative path at the top of the included source 88 | - The `random.seed` is always set to `1234` so your random functions have predictable output 89 | - The timezone is always set to `US/Pacific` so your code runs in the same timezone regardless of where your computer currently is located 90 | - The script will only be allowed to run for `--timeout_seconds` before being terminated (defaults to 5 seconds) 91 | 92 | ## TODO 93 | 94 | - Clean up the Python style 95 | - Write better docstrings 96 | - Write some tests (for things like `debug`, `pprint`, and `Pdb`) 97 | - Actually upload this to PyPI 98 | - Add a Travis build for example data 99 | -------------------------------------------------------------------------------- /example/Chapter1/Falafel_balls.md: -------------------------------------------------------------------------------- 1 | ## Falafel Balls 2 | 3 | This is a recipe for fried Falafel balls. 4 | 5 | ```python 6 | import math 7 | 8 | class MyFalafel(object): 9 | ``` 10 | 11 | Falafel balls require a few different methods. First you need to initialize them with a chosen diameter: 12 | 13 | ```python 14 | def __init__(self, diameter): 15 | self.diameter = diameter 16 | self.doneness = 0 17 | ``` 18 | 19 | Then you need to fry them the the oil. 20 | 21 | ```python 22 | def fry(self, minutes): 23 | self.doneness += minutes 24 | ``` 25 | 26 | You need to check to see if they're done based on the diameter of the falafel. 27 | 28 | ```python 29 | @property 30 | def done(self): 31 | if self.doneness == 0: 32 | return False 33 | volume = 4 / 3 * math.pi * (self.diameter / 2)**3 34 | return (volume / self.doneness) > 1 35 | ``` 36 | 37 | Now let's see that whole thing in action. 38 | 39 | ```python 40 | ball = MyFalafel(5) 41 | print('My falafel is', ball.diameter, 'inches in diameter') 42 | print('Is it done already?', ball.done) 43 | ball.fry(6) 44 | print('Is it done yet?', ball.done) 45 | ``` 46 | 47 | With the output: 48 | 49 | ``` 50 | My falafel is 5 inches in diameter 51 | Is it done already? False 52 | Is it done yet? True 53 | ``` 54 | 55 | And here are some things after the output. 56 | 57 | ``` 58 | This won't be changed. 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /example/Chapter1/Introduction.md: -------------------------------------------------------------------------------- 1 | # Fried food 2 | 3 | This is the introduction to the fried food chapter. 4 | 5 | -------------------------------------------------------------------------------- /example/Chapter1/Onion_rings.md: -------------------------------------------------------------------------------- 1 | ## Onion Rings 2 | 3 | This is all about onion rings. 4 | 5 | ```python-syntax-error 6 | class MyObject # Forgot a colon! 7 | def my_func(self): 8 | pass 9 | ``` 10 | 11 | Will show this syntax error: 12 | 13 | ``` 14 | SyntaxError: invalid syntax 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /example/Chapter2/Introduction.md: -------------------------------------------------------------------------------- 1 | # Fish 2 | 3 | This is the introduction to the fish chapter. 4 | 5 | -------------------------------------------------------------------------------- /example/Chapter2/Salmon_fillet.md: -------------------------------------------------------------------------------- 1 | ## Salmon fillet 2 | 3 | This is all about salmon. 4 | 5 | And here's a file that's included: 6 | 7 | ```python-include:chapter2/my_include.py 8 | # my_include.py 9 | a = 1234 10 | b = a ** 2 11 | 12 | def my_func(): 13 | return 5 14 | 15 | c = b * my_func() 16 | ``` 17 | 18 | But you'll have to copy the output yourself. 19 | 20 | -------------------------------------------------------------------------------- /example/Chapter2/Swordfish_steaks.md: -------------------------------------------------------------------------------- 1 | ## Swordfish steaks 2 | 3 | This is how to cook swordfish steaks. 4 | 5 | ```python2 6 | # Python 2 7 | print 'This will work' 8 | ``` 9 | 10 | And here's the output. 11 | 12 | ``` 13 | This will work 14 | ``` 15 | 16 | This is a Python exception: 17 | 18 | ```python-exception 19 | raise TypeError('Whoops!') 20 | ``` 21 | 22 | ``` 23 | TypeError: Whoops! 24 | ``` 25 | -------------------------------------------------------------------------------- /example/Chapter2/my_include.py: -------------------------------------------------------------------------------- 1 | a = 1234 2 | b = a ** 2 3 | 4 | def my_func(): 5 | return 5 6 | 7 | c = b * my_func() 8 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | OUTPUT_DIR = ./output 2 | ALL_INPUTS = $(shell find . -name '*.md') 3 | 4 | .PHONY: all build_dir 5 | .DEFAULT_GOAL := all 6 | 7 | $(OUTPUT_DIR)/%.md: %.md 8 | mkdir -p `dirname $@` 9 | cp $< $@ 10 | run_markdown --overwrite $@ 11 | - diff -C 3 $< $@ 12 | 13 | $(OUTPUT_DIR)/%.md: 14 | cat $^ > $@ 15 | 16 | $(OUTPUT_DIR)/Chapter1.md: \ 17 | $(OUTPUT_DIR)/Chapter1/Introduction.md \ 18 | $(OUTPUT_DIR)/Chapter1/Onion_rings.md \ 19 | $(OUTPUT_DIR)/Chapter1/Falafel_balls.md 20 | 21 | $(OUTPUT_DIR)/Chapter2.md: \ 22 | $(OUTPUT_DIR)/Chapter2/Introduction.md \ 23 | $(OUTPUT_DIR)/Chapter2/Salmon_fillet.md \ 24 | $(OUTPUT_DIR)/Chapter2/Swordfish_steaks.md 25 | 26 | $(OUTPUT_DIR)/Book_draft.md: \ 27 | $(OUTPUT_DIR)/Chapter1.md \ 28 | $(OUTPUT_DIR)/Chapter2.md 29 | 30 | run: 31 | run_markdown \ 32 | --overwrite \ 33 | $(ALL_INPUTS) 34 | 35 | crossreferences: 36 | cross_reference \ 37 | --overwrite \ 38 | $(ALL_INPUTS) 39 | 40 | clean: 41 | rm -Rf $(OUTPUT_DIR) 42 | -------------------------------------------------------------------------------- /pyliterate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bslatkin/pyliterate/0a964b841cfc4b397ae32066d6a441a7c8184d1c/pyliterate/__init__.py -------------------------------------------------------------------------------- /pyliterate/run_markdown.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2015 Brett Slatkin 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | """Rewrites a Markdown file by running the Python code embedded within it. 18 | 19 | Parses the Markdown file passed as the first command-line argument. Looks for 20 | code blocks like this: 21 | 22 | ```python 23 | print('Hello world') 24 | ``` 25 | 26 | Runs the code top-to-bottom in the file. When a non-Python block like 27 | this is found: 28 | 29 | ``` 30 | Output goes here 31 | ``` 32 | 33 | All of the prior Python blocks are combined together, run, and their combined 34 | output is inserted. Not every Python block needs to have an output block, but 35 | every output block needs at least one preceeding Python block to produce output. 36 | 37 | Python blocks do not have to be stand-alone code blocks. A single class or 38 | function definition can be interleaved with text. For example, this is legal: 39 | 40 | ```python 41 | def multiply(a, b): 42 | ``` 43 | 44 | And the body is: 45 | 46 | ```python 47 | return a * b 48 | ``` 49 | """ 50 | 51 | import argparse 52 | import ast 53 | import logging 54 | logging.getLogger().setLevel(logging.DEBUG) 55 | import io 56 | import os 57 | import pdb 58 | import pprint 59 | import pydoc 60 | import random 61 | import re 62 | import signal 63 | import subprocess 64 | import sys 65 | from time import tzset 66 | import traceback 67 | 68 | 69 | REAL_PRINT = print 70 | REAL_PPRINT = pprint.pprint 71 | 72 | 73 | class Flags(object): 74 | def __init__(self): 75 | self.parser = argparse.ArgumentParser( 76 | description='Rewrites a Markdown file by running the Python ' 77 | 'code embedded within it.') 78 | self.parser.add_argument( 79 | '--overwrite', 80 | action='store_true', 81 | default=False, 82 | help='Overwrite the file in-place if its contents ran ' 83 | 'successfully.') 84 | self.parser.add_argument( 85 | '--timeout_seconds', 86 | action='store', 87 | default=5, 88 | type=float, 89 | help='Kill the process with an error if the Python code in the ' 90 | 'Markdown file hasn\'t finished executing in this time.') 91 | self.parser.add_argument( 92 | 'path', 93 | action='store', 94 | default=None, 95 | nargs='+', 96 | help='Paths to the Markdown files to process.') 97 | self.parser.add_argument( 98 | '--root_dir', 99 | action='store', 100 | default='.', 101 | type=str, 102 | help='Path to the root directory of the book. Used to resolve ' 103 | '"python-include:" blocks in the source Markdown.') 104 | 105 | def parse(self): 106 | self.parser.parse_args(namespace=self) 107 | 108 | 109 | FLAGS = Flags() 110 | 111 | 112 | class MarkdownExecError(Exception): 113 | pass 114 | 115 | 116 | class WrappedException(Exception): 117 | def __init__(self, output=None): 118 | super().__init__() 119 | self.output = output 120 | 121 | 122 | def exec_source(path, source, context, raise_exceptions=False): 123 | output = io.StringIO() 124 | logging_handler = logging.StreamHandler(stream=output) 125 | 126 | def my_print(*args, **kwargs): 127 | kwargs['file'] = output 128 | REAL_PRINT(*args, **kwargs) 129 | 130 | def my_pprint(*args, **kwargs): 131 | kwargs['stream'] = output 132 | kwargs['width'] = 65 # Max width of monospace code lines in Word 133 | REAL_PPRINT(*args, **kwargs) 134 | 135 | def my_debug(*args, **kwargs): 136 | kwargs['file'] = sys.stderr 137 | REAL_PRINT(*args, **kwargs) 138 | 139 | def my_help(*args, **kwargs): 140 | helper = pydoc.Helper(output=output) 141 | return helper(*args, **kwargs) 142 | 143 | def my_pdb(): 144 | # Clear any alarm clocks since we're going interactive. 145 | signal.alarm(0) 146 | 147 | p = pdb.Pdb(stdin=sys.stdin, stdout=sys.stderr) 148 | p.use_rawinput = True 149 | return p 150 | 151 | context['print'] = my_print 152 | context['pprint'] = my_pprint 153 | context['debug'] = my_debug 154 | context['help'] = my_help 155 | context['Pdb'] = my_pdb 156 | context['STDOUT'] = output 157 | logging.getLogger().addHandler(logging_handler) 158 | try: 159 | node = ast.parse(source, path) 160 | code = compile(node, path, 'exec') 161 | exec(code, context, context) 162 | except Exception as e: 163 | # Restore the print functions for other code in this module. 164 | context['print'] = REAL_PRINT 165 | 166 | # If there's been any output so far, print it out before the 167 | # error traceback is printed so we have context for debugging. 168 | output_so_far = output.getvalue() 169 | if output_so_far: 170 | print(output_so_far, end='', file=sys.stderr) 171 | 172 | if raise_exceptions: 173 | raise WrappedException(output_so_far) from e 174 | 175 | format_list = traceback.format_exception(*sys.exc_info()) 176 | 177 | # Only show tracebacks for code that was in the target source file. 178 | # This way calls to parse(), compile(), and exec() are removed. 179 | cutoff = 0 180 | for cutoff, line in enumerate(format_list): 181 | if path in line: 182 | break 183 | 184 | formatted = ''.join(format_list[cutoff:]) 185 | print('Traceback (most recent call last):', file=sys.stderr) 186 | print(formatted, end='', file=sys.stderr) 187 | raise MarkdownExecError(e) 188 | finally: 189 | # Restore the print function for other code in this module. 190 | context['print'] = REAL_PRINT 191 | # Disable the logging handler. 192 | logging.getLogger().removeHandler(logging_handler) 193 | 194 | return output.getvalue() 195 | 196 | 197 | def exec_python2(source): 198 | source_bytes = source.encode('utf-8') 199 | output = subprocess.check_output(['python2.7'], input=source_bytes) 200 | return output.decode('utf-8') 201 | 202 | 203 | def exec_syntax_error(source): 204 | source_bytes = source.encode('utf-8') 205 | child = subprocess.Popen( 206 | ['python3'], 207 | stdin=subprocess.PIPE, 208 | stderr=subprocess.PIPE) 209 | _, output = child.communicate(input=source_bytes) 210 | output = output.decode('utf-8') 211 | # Only return the last line. The preceeding lines will be parts of 212 | # the stack trace that caused the syntax error. 213 | return output.strip('\n').split('\n')[-1] 214 | 215 | 216 | filename_re = re.compile('[\\.]{0,2}/[-a-zA-Z0-9_/]+(\\.py|\\.md)') 217 | 218 | 219 | def exec_exception(path, source, context): 220 | try: 221 | exec_source(path, source, context, 222 | raise_exceptions=True) 223 | except WrappedException as e: 224 | original = e.__context__ 225 | pretty_exception = str(original) 226 | pretty_exception = filename_re.sub('my_code.py', pretty_exception) 227 | exception_line = '%s: %s' % ( 228 | original.__class__.__name__, pretty_exception) 229 | 230 | if e.output: 231 | return '%sTraceback ...\n%s' % ( 232 | e.output, exception_line) 233 | else: 234 | return exception_line 235 | else: 236 | assert False, 'Exception not raised' 237 | 238 | 239 | def iterate_blocks(path, text): 240 | text_start = 0 241 | source_start = None 242 | block_suffix = '' 243 | pending_source = '' 244 | pending_output = '' 245 | 246 | # Import the __main__ module, which is this file, and run all of the 247 | # Markdown code as if it's part of that module. This enables modules 248 | # like pickle to work, which need to use import paths relative to named 249 | # modules in order to serialized/deserialize functions. 250 | import __main__ 251 | context = __main__.__dict__ 252 | 253 | for blocks_seen, match in enumerate(re.finditer('```', text)): 254 | start = match.start() 255 | end = match.end() 256 | 257 | if blocks_seen % 2 == 0: 258 | # Figure out the language of the opening block. 259 | suffix_end = text.find('\n', end) 260 | if suffix_end > 0: 261 | block_suffix = text[end:suffix_end].lower() 262 | source_start = end + len(block_suffix) 263 | 264 | # All text until the block start 265 | yield text[text_start:start] 266 | else: 267 | text_start = end 268 | source = text[source_start:start] 269 | 270 | # Closing block 271 | if block_suffix in ('python', 'python-exception'): 272 | # Run any pending source immediately if this is a Python 273 | # exception block to ensure we re-raise unexpected exceptions 274 | # back up instead of just printing them into output blocks. 275 | if block_suffix == 'python-exception': 276 | if pending_source: 277 | pending_output += exec_source( 278 | path, pending_source, context) 279 | pending_source = '' 280 | 281 | # Add a bunch of blank lines before the source to get the line 282 | # numbers to match up during ast.parse. The ast.increment_lineno 283 | # helper is useful, but only after parsing was successful. If 284 | # you want helpful error messages during parsing you need to 285 | # fake it. 286 | line_offset = text[:source_start].count('\n') 287 | delta_offset = line_offset - pending_source.count('\n') 288 | pending_source += '\n' * delta_offset 289 | 290 | # Accumulate all the source code to run until we reach the 291 | # first output block *or* the end of the file. 292 | pending_source += source 293 | 294 | yield '```%s' % block_suffix 295 | yield source 296 | yield '```' 297 | 298 | if block_suffix == 'python-exception': 299 | pending_output += exec_exception( 300 | path, pending_source, context) 301 | pending_source = '' 302 | elif block_suffix == 'python2': 303 | yield '```python2' 304 | if not source.startswith('\n# Python 2'): 305 | yield '\n# Python 2' 306 | yield source 307 | yield '```' 308 | 309 | pending_output += exec_python2(source) 310 | elif block_suffix == 'python-syntax-error': 311 | yield '```python-syntax-error' 312 | yield source 313 | yield '```' 314 | 315 | pending_output += exec_syntax_error(source) 316 | elif block_suffix.startswith('python-include:'): 317 | include_path = block_suffix[len('python-include:'):] 318 | file_basename = os.path.basename(include_path) 319 | full_path = os.path.join(FLAGS.root_dir, include_path) 320 | data = open(full_path, 'r').read() 321 | 322 | yield '```%s\n' % block_suffix 323 | yield '# %s\n' % file_basename 324 | yield data.strip() 325 | yield '\n```' 326 | else: 327 | if pending_source: 328 | pending_output += exec_source( 329 | path, pending_source, context) 330 | pending_source = '' 331 | elif not pending_output: 332 | # This handles random output blocks in the Markdown file 333 | # that do not follow Python blocks. This is helpful when 334 | # you're sketching out a rough draft. Just pass through 335 | # whatever the field was before. 336 | pending_output = source 337 | 338 | # Output block 339 | yield '```%s\n' % block_suffix 340 | yield pending_output.strip('\n') 341 | yield '\n```' 342 | 343 | pending_output = '' 344 | 345 | # What follows the very last block to the end of the file 346 | yield text[text_start:] 347 | 348 | # Run any remaining pending code to make sure it doesn't have errors. 349 | if pending_source: 350 | exec_source(path, pending_source, context) 351 | 352 | 353 | def print_iter(it, path, overwrite): 354 | output = sys.stdout 355 | if overwrite: 356 | output = io.StringIO() 357 | try: 358 | for text in it: 359 | print(text, end='', file=output) 360 | except: 361 | raise 362 | else: 363 | if overwrite: 364 | open(path, 'w', encoding='utf-8').write(output.getvalue()) 365 | 366 | 367 | def main(): 368 | FLAGS.parse() 369 | 370 | for path in FLAGS.path: 371 | # Always use the same seed so multiple runs of the same Markdown files 372 | # will always produce the same output results. 373 | random.seed(1234) 374 | 375 | # Always pretend that we're running in Pacific Time 376 | os.environ['TZ'] = 'US/Pacific' 377 | tzset() 378 | 379 | # Kill the process if it's been running for longer than the timeout. 380 | signal.alarm(FLAGS.timeout_seconds) 381 | 382 | text = open(path, encoding='utf-8').read() 383 | it = iterate_blocks(path, text) 384 | try: 385 | print_iter(it, path, FLAGS.overwrite) 386 | except MarkdownExecError: 387 | return 1 388 | 389 | return 0 390 | 391 | 392 | if __name__ == '__main__': 393 | sys.exit(main()) 394 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name = 'pyliterate', 7 | packages = ['pyliterate'], 8 | version = '0.1', 9 | description = 'Literate programming with Python', 10 | author = 'Brett Slatkin', 11 | author_email = 'brett@haxor.com', 12 | url = 'https://github.com/bslatkin/pyliterate', 13 | keywords = [], 14 | entry_points={ 15 | 'console_scripts': [ 16 | 'run_markdown=pyliterate.run_markdown:main', 17 | ] 18 | }, 19 | classifiers = [ 20 | ], 21 | long_description = """\ 22 | """ 23 | ) 24 | --------------------------------------------------------------------------------