├── tests ├── __init__.py ├── hidden_filename.py ├── roots │ └── test-ext-execute-code │ │ ├── conf.py │ │ └── index.rst ├── conftest.py ├── example_class.py └── test_execute_code.py ├── AUTHORS ├── .travis.yml ├── MANIFEST.in ├── MANIFEST ├── tox.ini ├── setup.py ├── LICENSE.txt ├── .gitignore ├── Readme.rst └── sphinx_execute_code └── __init__.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | JP Senior 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language : python 2 | 3 | python: 4 | - 2.7 5 | 6 | install: 7 | - pip install tox-travis 8 | script: 9 | - tox -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt AUTHORS Readme.rst 2 | recursive-include sphinx_execute_code *.py 3 | recursive-include tests *.py -------------------------------------------------------------------------------- /tests/hidden_filename.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/python 2 | """ Used for unit testing 3 | """ 4 | print 'execute_code_hide_filename:' + 'sample_9' 5 | -------------------------------------------------------------------------------- /tests/roots/test-ext-execute-code/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | master_doc = 'index' 5 | exclude_patterns = ['_build'] 6 | sys.path.insert(0, os.path.abspath('.')) 7 | import sphinx_execute_code 8 | extensions = ['sphinx_execute_code'] -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | AUTHORS 3 | LICENSE.txt 4 | Readme.rst 5 | setup.py 6 | sphinx_execute_code\__init__.py 7 | tests\__init__.py 8 | tests\conftest.py 9 | tests\example_class.py 10 | tests\test_execute_code.py 11 | tests\roots\test-ext-execute-code\conf.py 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py27 4 | lint 5 | 6 | [testenv] 7 | deps = 8 | pytest 9 | sphinx 10 | docutils 11 | commands=pytest 12 | 13 | [testenv:lint] 14 | basepython=python2.7 15 | deps = 16 | pylint 17 | pytest 18 | doc8 19 | docutils 20 | commands = 21 | pylint tests 22 | pylint sphinx_execute_code 23 | pylint setup.py 24 | doc8 Readme.rst 25 | doc8 tests/ 26 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ Set up sphinx pytest modules and fixtures 3 | """ 4 | import os 5 | 6 | import pytest 7 | from sphinx.testing.path import path 8 | 9 | # pylint: disable=invalid-name 10 | pytest_plugins = 'sphinx.testing.fixtures' 11 | 12 | @pytest.fixture(scope='session') 13 | def rootdir(): 14 | """ Sets root folder for sphinx unit tests """ 15 | return path(os.path.dirname(__file__) or '.').abspath() / 'roots' 16 | -------------------------------------------------------------------------------- /tests/example_class.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ Example program used by execute_code functions """ 3 | # Example comment for unit testing 4 | 5 | # pylint: disable=too-few-public-methods 6 | class Hello(object): 7 | """ Simple class to show imports """ 8 | def __init__(self): 9 | self.msg = 'Hello, ' + 'world!' 10 | 11 | def out(self): 12 | """ returns Hello, world!""" 13 | return self.msg 14 | 15 | if __name__ == "__main__": 16 | print Hello().out() 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ Distutils setup file """ 3 | from setuptools import setup 4 | 5 | def readme(): 6 | """ Returns Readme.rst as loaded RST for documentation """ 7 | with open('Readme.rst', 'r') as filename: 8 | return filename.read() 9 | setup( 10 | name='sphinx_execute_code', 11 | version='0.2a2', 12 | platforms=['any'], 13 | packages=['sphinx_execute_code'], 14 | url='https://github.com/jpsenior/sphinx-execute-code', 15 | license='MIT', 16 | author='JP Senior', 17 | author_email='jp.senior@gmail.com', 18 | description='Sphinx support for execution of python code from code blocks or files', 19 | long_description=readme(), 20 | install_requires=['docutils', 'sphinx'], 21 | classifiers=[ 22 | 'Development Status :: 3 - Alpha', 23 | 'Intended Audience :: Developers', 24 | 'Framework :: Sphinx :: Extension', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Natural Language :: English', 27 | 'Programming Language :: Python :: 2.7', 28 | ], 29 | keywords='sphinx extension directive execute code', 30 | ) 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 JP Senior (jp.senior@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /tests/roots/test-ext-execute-code/index.rst: -------------------------------------------------------------------------------- 1 | tests 2 | ===== 3 | 4 | This is a test 5 | 6 | .. execute_code:: 7 | 8 | print 'execute_code:' + 'sample_1' 9 | 10 | .. execute_code:: 11 | :linenos: 12 | 13 | print 'execute_code_linenos:' + 'sample_2' 14 | 15 | .. execute_code:: 16 | :output_language: javascript 17 | 18 | data = {'execute_code': 'sample_3', 'output_language': 'javascript', 'sample3': True} 19 | import json 20 | print json.dumps(data) 21 | 22 | .. sample_4: 23 | 24 | .. execute_code:: 25 | :filename: tests/example_class.py 26 | 27 | .. execute_code:: 28 | :filename: tests/example_class.py 29 | 30 | print 'execute_code_should_not_run:' + 'sample_5' 31 | #execute_code_sample_5_comment_is_hidden 32 | 33 | .. execute_code:: 34 | :hide_code: 35 | 36 | print 'execute_code_hide_code:sample_6' 37 | # This comment is hidden 38 | 39 | 40 | .. execute_code:: 41 | 42 | print 'execute_code_show_header:' + 'sample_7' 43 | 44 | .. execute_code:: 45 | :hide_headers: 46 | 47 | print 'execute_code_hide_header:' + 'sample_8' 48 | 49 | .. execute_code:: 50 | :filename: tests/hidden_filename.py 51 | :hide_filename: 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .tox 3 | .cache 4 | build/ 5 | *.pyc 6 | dist/ 7 | ### Python template 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | -------------------------------------------------------------------------------- /tests/test_execute_code.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | test_execute_code 4 | ================= 5 | 6 | Tests the execute_code directive 7 | 8 | :copyright: Copyright 2017, JP Senior 9 | :license: MIT 10 | 11 | """ 12 | import json 13 | from textwrap import dedent 14 | import pytest 15 | from sphinx_execute_code import ExecuteCode 16 | from tests.example_class import Hello 17 | 18 | 19 | 20 | 21 | def test_execute_code_function(): 22 | """ Ensure simple code functions execute """ 23 | code = dedent(''' 24 | print "foo" 25 | print "bar" 26 | ''') 27 | 28 | expected_output = dedent('''\ 29 | foo 30 | bar 31 | ''') 32 | results = ExecuteCode.execute_code(code) 33 | 34 | assert expected_output == results 35 | 36 | def test_execute_and_import(): 37 | """ Import a generic module, make sure we do not get any type errors """ 38 | code = dedent(''' 39 | import os 40 | print os.path 41 | ''') 42 | results = ExecuteCode.execute_code(code) 43 | 44 | assert results != None 45 | assert results != '' 46 | 47 | def test_execute_empty(): 48 | """ execute_code function should be able to take empty content """ 49 | code = '' 50 | results = ExecuteCode.execute_code(code) 51 | assert results == '' 52 | 53 | def test_execute_code_sample(): 54 | """ Just makes sure the sample code output of this sample works as expected """ 55 | hello = Hello() 56 | assert hello.out() == 'Hello, world!' 57 | 58 | 59 | # sphinx tests 60 | # pylint: disable=unused-argument 61 | @pytest.mark.sphinx('text', testroot='ext-execute-code', freshenv=True, 62 | confoverrides={}) 63 | def test_sphinx_execute_code(app, status, warning): 64 | """ Runs a sphinx 'text' renderer and uses roots/test-ext-execute-code/index.rst 65 | to perform specific code rendering unit tests 66 | """ 67 | app.builder.build_all() 68 | 69 | if app.statuscode != 0: 70 | assert False, 'Failures in execute_code: ' + status.getvalue() 71 | content = (app.outdir / 'index.txt').text() 72 | 73 | # Make sure the module is loaded 74 | assert 'sphinx_execute_code' in app.extensions 75 | 76 | # Ensure sample_1 executes 77 | assert 'execute_code:sample_1' in content 78 | 79 | # Ensure linenos argument is accepted 80 | assert 'execute_code_linenos:sample_2' in content 81 | 82 | # Ensure that we can format output language properly 83 | data = {'execute_code': 'sample_3', 'output_language': 'javascript', 'sample3': True} 84 | 85 | assert json.dumps(data) in content 86 | assert 'Hello, world!' in content 87 | assert 'Example comment for unit testing' in content 88 | 89 | # Ensure :hidecode: works to hide example code 90 | assert '#execute_code_sample_5_comment_is_hidden' not in content 91 | assert 'execute_code_should_not_run' not in content 92 | 93 | # Ensure :hidecode: works 94 | assert 'execute_code_hide_code:sample_6' in content 95 | assert 'This comment is hidden' not in content 96 | 97 | # Ensure headers render 98 | results_header = dedent('''\ 99 | Results 100 | 101 | execute_code_show_header:sample_7 102 | ''') 103 | 104 | assert results_header in content 105 | 106 | results_header = dedent('''\ 107 | Results 108 | 109 | execute_code_hide_header:sample_8 110 | ''') 111 | 112 | # Ensure filename is hidden with :hide_filename: 113 | assert results_header not in content 114 | 115 | assert 'execute_code_hide_filename:sample_9' in content 116 | assert 'hidden_filename' not in content 117 | -------------------------------------------------------------------------------- /Readme.rst: -------------------------------------------------------------------------------- 1 | sphinx-execute-code 2 | =================== 3 | 4 | Sphinx-execute-code is an extension for Sphinx that allows a document author 5 | to insert arbitrary python code samples in code blocks, or run python code 6 | from python files on the filesystem. 7 | 8 | This was written as an alternative to other code execution functions which 9 | relied on doctest formats, and attempts to be more flexible, similar to 10 | literal-block and code-block statements. 11 | 12 | .. warning:: This module allows you to run arbitrary code, and should be treated with caution. 13 | 14 | Options 15 | ------- 16 | Options right now (as of version 0.2) are: 17 | 18 | linenos 19 | If specified, will show line numbers 20 | output_language 21 | Customizes pygments lexxer for specified language (Eg, Javascript, bash) 22 | hide_code 23 | If specified, will hide the code block and only show results 24 | hide_headers 25 | If specified, hides the 'Code' and 'Results' caption headers around 26 | the literal blocks 27 | filename 28 | If specified, will load code from a file (relative to sphinx doc root) 29 | and ignore content. 30 | 31 | execute_code 32 | ------------ 33 | Running 'execute_code' as a directive allows the administrator to embed exact 34 | python code as if it was pasted in a normal code-block. 35 | 36 | Executing python code and showing the result output:: 37 | 38 | .. execute_code:: 39 | :linenos: 40 | 41 | print 'python highlight code' 42 | 43 | class Foo(object): 44 | def __init__(self): 45 | self.bar = 'baz' 46 | def out(self): 47 | print self.bar 48 | 49 | f = Foo() 50 | f.out() 51 | 52 | Output language 53 | --------------- 54 | Customizing the output syntax can be helpful to make it easy to document 55 | any other pygments lexxer - eg ini, javascript 56 | 57 | We can customize the output language parser (for JSON/Javascript):: 58 | 59 | .. execute_code:: 60 | :output_language: javascript 61 | 62 | print "'{foo-bar-baz}'" 63 | 64 | Hiding code 65 | ----------- 66 | You may want to hide the example code that is executing 67 | (avoiding highlighting/etc) and display the results only. 68 | 69 | We can also hide the code input, showing only the executed code results:: 70 | 71 | .. execute_code:: 72 | :hide_code: 73 | 74 | print 'This should not print the example code' 75 | 76 | Suppressing output headers 77 | -------------------------- 78 | Suppressing the 'Headers' outputs for Code and Results header:: 79 | 80 | .. execute_code:: 81 | :hide_headers: 82 | 83 | foo = 32 84 | print 'This will hide the Code and Results text - and foo is %d' % foo 85 | 86 | Executing python code from a file 87 | --------------------------------- 88 | execute_code also allows you to import a python file and execute 89 | it within a document. 90 | 91 | Running a Python file from filename from the .py example:: 92 | 93 | .. execute_code:: 94 | :filename: tests/example_class.py 95 | 96 | This function also supports the argument 'hide_filename':: 97 | 98 | .. execute_code:: 99 | :filename: tests/example_class.py 100 | :hide_filename: 101 | 102 | Installation 103 | ============ 104 | 105 | Installation from source:: 106 | 107 | $ git clone git@github.com:jpsenior/sphinx-execute-code.git 108 | $ python setup.py install 109 | 110 | Installation from pypi:: 111 | 112 | $ pip install sphinx-execute-code 113 | 114 | Activating on Sphinx 115 | ==================== 116 | 117 | To activate the extension, add it to your extensions variable in conf.py 118 | for your project. 119 | 120 | Activating the extension in sphinx:: 121 | 122 | extensions.append('sphinx_execute_code') 123 | -------------------------------------------------------------------------------- /sphinx_execute_code/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/python 2 | """ 3 | sphinx-execute-code module for execute_code directive 4 | To use this module, add: extensions.append('sphinx_execute_code') 5 | 6 | Available options: 7 | 8 | 'linenos': directives.flag, 9 | 'output_language': directives.unchanged, 10 | 'hide_code': directives.flag, 11 | 'hide_headers': directives.flag, 12 | 'filename': directives.path, 13 | 'hide_filename': directives.flag, 14 | 15 | Usage: 16 | 17 | .. example_code: 18 | :linenos: 19 | :hide_code: 20 | 21 | print 'Execute this python code' 22 | 23 | See Readme.rst for documentation details 24 | """ 25 | import sys 26 | import os 27 | from docutils.parsers.rst import Directive, directives 28 | from docutils import nodes 29 | 30 | if sys.version_info.major == 2: 31 | from StringIO import StringIO 32 | else: 33 | from io import StringIO 34 | 35 | # execute_code function thanks to Stackoverflow code post from hekevintran 36 | # https://stackoverflow.com/questions/701802/how-do-i-execute-a-string-containing-python-code-in-python 37 | 38 | __author__ = 'jp.senior@gmail.com' 39 | __docformat__ = 'restructuredtext' 40 | __version__ = '0.2a2' 41 | 42 | 43 | class ExecuteCode(Directive): 44 | """ Sphinx class for execute_code directive 45 | """ 46 | has_content = True 47 | required_arguments = 0 48 | optional_arguments = 5 49 | 50 | option_spec = { 51 | 'linenos': directives.flag, 52 | 'output_language': directives.unchanged, # Runs specified pygments lexer on output data 53 | 'hide_code': directives.flag, 54 | 'hide_headers': directives.flag, 55 | 'filename': directives.path, 56 | 'hide_filename': directives.flag, 57 | } 58 | 59 | @classmethod 60 | def execute_code(cls, code): 61 | """ Executes supplied code as pure python and returns a list of stdout, stderr 62 | 63 | Args: 64 | code (string): Python code to execute 65 | 66 | Results: 67 | (list): stdout, stderr of executed python code 68 | 69 | Raises: 70 | ExecutionError when supplied python is incorrect 71 | 72 | Examples: 73 | >>> execute_code('print "foobar"') 74 | 'foobar' 75 | """ 76 | 77 | output = StringIO.StringIO() 78 | err = StringIO.StringIO() 79 | 80 | sys.stdout = output 81 | sys.stderr = err 82 | 83 | try: 84 | # pylint: disable=exec-used 85 | exec(code) 86 | # If the code is invalid, just skip the block - any actual code errors 87 | # will be raised properly 88 | except TypeError: 89 | pass 90 | sys.stdout = sys.__stdout__ 91 | sys.stderr = sys.__stderr__ 92 | 93 | results = list() 94 | results.append(output.getvalue()) 95 | results.append(err.getvalue()) 96 | results = ''.join(results) 97 | 98 | return results 99 | 100 | def run(self): 101 | """ Executes python code for an RST document, taking input from content or from a filename 102 | 103 | :return: 104 | """ 105 | language = self.options.get('language') or 'python' 106 | output_language = self.options.get('output_language') or 'none' 107 | filename = self.options.get('filename') 108 | code = '' 109 | 110 | if not filename: 111 | code = '\n'.join(self.content) 112 | if filename: 113 | try: 114 | with open(filename, 'r') as code_file: 115 | code = code_file.read() 116 | self.warning('code is %s' % code) 117 | except (IOError, OSError) as err: 118 | # Raise warning instead of a code block 119 | error = 'Error opening file: %s, working folder: %s' % (err, os.getcwd()) 120 | self.warning(error) 121 | return [nodes.warning(error, error)] 122 | 123 | output = [] 124 | 125 | # Show the example code 126 | if not 'hide_code' in self.options: 127 | input_code = nodes.literal_block(code, code) 128 | 129 | input_code['language'] = language 130 | input_code['linenos'] = 'linenos' in self.options 131 | if not 'hide_headers' in self.options: 132 | suffix = '' 133 | if not 'hide_filename' in self.options: 134 | suffix = '' if filename is None else str(filename) 135 | output.append(nodes.caption( 136 | text='Code %s' % suffix)) 137 | output.append(input_code) 138 | 139 | # Show the code results 140 | if not 'hide_headers' in self.options: 141 | output.append(nodes.caption(text='Results')) 142 | code_results = self.execute_code(code) 143 | code_results = nodes.literal_block(code_results, code_results) 144 | 145 | code_results['linenos'] = 'linenos' in self.options 146 | code_results['language'] = output_language 147 | output.append(code_results) 148 | return output 149 | 150 | def setup(app): 151 | """ Register sphinx_execute_code directive with Sphinx """ 152 | app.add_directive('execute_code', ExecuteCode) 153 | return {'version': __version__} 154 | --------------------------------------------------------------------------------