├── setup.cfg ├── MANIFEST.in ├── src └── tut │ ├── tests │ ├── __init__.py │ ├── abs_path │ │ ├── index.txt │ │ └── conf.py │ ├── root │ │ ├── index.txt │ │ └── conf.py │ ├── relative_path │ │ ├── index.txt │ │ └── conf.py │ ├── tut_directive │ │ ├── index.txt │ │ └── conf.py │ ├── literalinclude │ │ ├── index.txt │ │ └── conf.py │ ├── test_sphinx_code.py │ ├── test_sphinx.py │ ├── test_model.py │ └── test_diffs.py │ ├── __init__.py │ ├── sphinx │ ├── __init__.py │ ├── manager.py │ ├── checkpoint.py │ ├── content.py │ └── code.py │ ├── cmd.py │ ├── model.py │ └── diff.py ├── tox.ini ├── .gitignore ├── .bumpversion.cfg ├── HACKING.txt ├── .travis.yml ├── setup.py ├── NEWS.txt ├── bootstrap.py └── README.rst /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include NEWS.txt 3 | -------------------------------------------------------------------------------- /src/tut/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | def additional_tests(): 4 | return unittest.defaultTestLoader.discover('.') 5 | -------------------------------------------------------------------------------- /src/tut/__init__.py: -------------------------------------------------------------------------------- 1 | def version(): 2 | """Return the installed package version.""" 3 | 4 | import pkg_resources 5 | 6 | return pkg_resources.get_distribution('tut').version 7 | -------------------------------------------------------------------------------- /src/tut/tests/abs_path/index.txt: -------------------------------------------------------------------------------- 1 | ================== 2 | Absolute Path Test 3 | ================== 4 | 5 | Test Document 6 | 7 | .. checkpoint:: step_one 8 | :path: /src 9 | 10 | Further content 11 | -------------------------------------------------------------------------------- /src/tut/tests/root/index.txt: -------------------------------------------------------------------------------- 1 | =================== 2 | Tut Test Document 3 | =================== 4 | 5 | Test Document 6 | 7 | .. checkpoint:: step_one 8 | :path: /src 9 | 10 | Further content 11 | -------------------------------------------------------------------------------- /src/tut/tests/relative_path/index.txt: -------------------------------------------------------------------------------- 1 | ================== 2 | Relative Path Test 3 | ================== 4 | 5 | Test Document 6 | 7 | .. checkpoint:: step_one 8 | :path: src 9 | 10 | Further content 11 | -------------------------------------------------------------------------------- /src/tut/tests/tut_directive/index.txt: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: /src 3 | 4 | ================== 5 | Tut Directive Test 6 | ================== 7 | 8 | Test Document 9 | 10 | .. checkpoint:: step_one 11 | 12 | Further content 13 | -------------------------------------------------------------------------------- /src/tut/tests/literalinclude/index.txt: -------------------------------------------------------------------------------- 1 | ================== 2 | Tut Directive Test 3 | ================== 4 | 5 | .. tut:checkpoint:: step_one 6 | :path: /testing 7 | 8 | Test Document 9 | 10 | .. tut:literalinclude:: /testing/setup.py 11 | 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = {py36}-sphinx-{16,dev} 3 | 4 | [testenv] 5 | pip_pre = True 6 | basepython = 7 | py36: python3.6 8 | deps = 9 | sphinx-16: Sphinx~=1.6.0 10 | sphinx-dev: git+https://github.com/sphinx-doc/sphinx.git#egg=Sphinx-dev 11 | 12 | commands=python setup.py test 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | tags 4 | 5 | .installed.cfg 6 | bin/ 7 | include/ 8 | lib/ 9 | man/ 10 | share/ 11 | .Python 12 | pip-selfcheck.json 13 | 14 | develop-eggs/ 15 | 16 | *.egg-info 17 | 18 | tmp/ 19 | build/ 20 | dist/ 21 | eggs/ 22 | *.egg 23 | .tox/ 24 | /.eggs/README.txt 25 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.5.1 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:NEWS.txt] 9 | search = 10 | DEVELOPMENT 11 | replace = 12 | DEVELOPMENT 13 | 14 | (unreleased) 15 | 16 | ... 17 | 18 | 19 | {new_version} 20 | -------- 21 | 22 | *Release Date: {now:%d %B %Y}* 23 | 24 | -------------------------------------------------------------------------------- /HACKING.txt: -------------------------------------------------------------------------------- 1 | Development setup 2 | ================= 3 | 4 | To create a buildout, 5 | 6 | $ python bootstrap.py 7 | $ bin/buildout 8 | 9 | Release HOWTO 10 | ============= 11 | 12 | To make a release, 13 | 14 | 1) Update release date/version in NEWS.txt and setup.py 15 | 2) Run 'python setup.py sdist' 16 | 3) Test the generated source distribution in dist/ 17 | 4) Upload to PyPI: 'python setup.py sdist register upload' 18 | 5) Increase version in setup.py (for next release) 19 | 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: python 3 | python: 4 | - "3.6" 5 | 6 | env: 7 | - SPHINX_SPEC=Sphinx~=1.6.0 8 | - SPHINX_SPEC=git+https://github.com/sphinx-doc/sphinx.git#egg=Sphinx-dev 9 | 10 | install: 11 | - pip install coveralls 12 | - pip install $SPHINX_SPEC 13 | - pip install git+https://github.com/nyergler/sphinx-testing.git 14 | - python setup.py install 15 | 16 | before_script: 17 | - git config --global user.email "nathan+travis@yergler.net" 18 | - git config --global user.name "Travis Build" 19 | 20 | script: 21 | - coverage run --source=tut setup.py test 22 | 23 | after_success: 24 | - coveralls 25 | -------------------------------------------------------------------------------- /src/tut/sphinx/__init__.py: -------------------------------------------------------------------------------- 1 | from docutils.nodes import reference 2 | 3 | from tut import version 4 | from tut.sphinx.checkpoint import ( 5 | TutDefaults, 6 | TutCheckpoint, 7 | initialize, 8 | cleanup, 9 | ) 10 | # from tut.sphinx.content import ( 11 | # TutContent, 12 | # TutExec, 13 | # ) 14 | from tut.sphinx.code import ( 15 | TutCodeDiff, 16 | TutLiteralInclude, 17 | ) 18 | 19 | 20 | def setup(app): 21 | 22 | app.add_directive('tut', TutDefaults) 23 | # app.add_directive('tut:exec', TutExec) 24 | # app.add_directive('tut:content', TutContent) 25 | app.add_directive('checkpoint', TutCheckpoint) 26 | app.add_directive('tut:checkpoint', TutCheckpoint) 27 | app.add_directive('tut:literalinclude', TutLiteralInclude) 28 | app.add_directive('tut:diff', TutCodeDiff) 29 | 30 | app.connect('builder-inited', initialize) 31 | app.connect('build-finished', cleanup) 32 | 33 | return { 34 | 'version': version(), 35 | 'parallel_read_safe': False, 36 | } 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import sys, os 3 | 4 | here = os.path.abspath(os.path.dirname(__file__)) 5 | README = open(os.path.join(here, 'README.rst')).read() 6 | NEWS = open(os.path.join(here, 'NEWS.txt')).read() 7 | 8 | 9 | version = '0.5.1' 10 | 11 | install_requires = [ 12 | 'docopt', 13 | 'pyyaml', 14 | 'sh', 15 | 'Sphinx>=1.6.0', 16 | ] 17 | 18 | 19 | setup(name='tut', 20 | version=version, 21 | description="", 22 | long_description=README + '\n\n' + NEWS, 23 | classifiers=[ 24 | ], 25 | keywords='', 26 | author='Nathan Yergler', 27 | author_email='nathan@yergler.net', 28 | url='http://github.com/nyergler/tut', 29 | license='BSD', 30 | packages=find_packages('src'), 31 | package_dir={'': 'src'}, 32 | include_package_data=True, 33 | zip_safe=False, 34 | install_requires=install_requires, 35 | entry_points={ 36 | 'console_scripts': [ 37 | 'tut=tut.cmd:main', 38 | ], 39 | }, 40 | tests_require=[ 41 | 'munch', 42 | 'sphinx-testing>=0.7.2', 43 | ], 44 | test_suite='tut.tests', 45 | ) 46 | -------------------------------------------------------------------------------- /NEWS.txt: -------------------------------------------------------------------------------- 1 | 2 | News 3 | ==== 4 | 5 | DEVELOPMENT 6 | 7 | (unreleased) 8 | 9 | ... 10 | 11 | 12 | 0.5.1 13 | -------- 14 | 15 | *Release Date: 30 April 2017* 16 | 17 | * Fixed missing import which caused tut:literalinclude to silently fails 18 | 19 | 0.5.0 20 | ----- 21 | 22 | *Release Date: 30 April 2017* 23 | 24 | * Addition of ``tut:literalinclude`` and ``tut:diff`` directives 25 | * Sphinx directives are namespaced under ``tut:`` 26 | * Drop support for Sphinx releases prior to 1.6 27 | * Drop support for Python 2 28 | * Use dedicated config file on special branch for maintaining point 29 | list. 30 | * Added ``tut fetch`` to support retreiving all checkpoints. 31 | * Better error reporting when calling git fails. 32 | 33 | 0.2 34 | --- 35 | 36 | *Release date: 11 April 2013* 37 | 38 | * BACKWARDS INCOMPATIBLE 39 | * Removed post-rewrite hook, ``tut-remap`` 40 | * Moved from tag-based checkpoints to branch-based 41 | * Added ``next`` sub-command to move from one step to the next 42 | * ``edit`` now checks out a branch 43 | 44 | 0.1 45 | --- 46 | 47 | *Release date: 17 March 2013* 48 | 49 | * Support for switching to tags, branches, etc within Sphinx documents 50 | * Initial implementation of wrapper script 51 | -------------------------------------------------------------------------------- /src/tut/tests/test_sphinx_code.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from unittest import TestCase 3 | from unittest.mock import patch 4 | 5 | from sphinx_testing import with_app 6 | from sphinx_testing.path import path 7 | 8 | from tut.sphinx.manager import TutManager 9 | 10 | 11 | test_root = path(__file__).parent.joinpath('root').abspath().parent 12 | 13 | 14 | @patch('tut.sphinx.code.TutManager.get') 15 | class TutLiteralIncludeTests(TestCase): 16 | 17 | @with_app(srcdir=test_root/'literalinclude') 18 | def test_literalinclude_fetches_from_git(self, git_mock, sphinx_app, status, warning): 19 | 20 | git_mock().configure_mock(**{ 21 | 'resolve_option.return_value': '/testing', 22 | }) 23 | git_mock().tut().configure_mock(**{ 24 | 'file.return_value': b'foobar', 25 | 'path': test_root/'literalinclude'/'testing', 26 | }) 27 | sphinx_app.builder.build_all() 28 | 29 | self.assertEqual(git_mock().tut().file.call_count, 1) 30 | self.assertEqual( 31 | git_mock().tut().file.call_args[0], 32 | ('step_one', 'setup.py'), 33 | ) 34 | 35 | # ensure the content was correctly included 36 | with open(os.path.join(sphinx_app.builddir, 'html', 'index.html')) as output_html: 37 | self.assertIn('foobar', output_html.read()) 38 | -------------------------------------------------------------------------------- /src/tut/sphinx/manager.py: -------------------------------------------------------------------------------- 1 | from tut.model import Tut 2 | 3 | 4 | class UNSET(object): 5 | pass 6 | UNSET = UNSET() 7 | 8 | 9 | class TutManager(object): 10 | 11 | @classmethod 12 | def get(cls, env): 13 | if not hasattr(env, '_tut_mgr'): 14 | env._tut_mgr = cls() 15 | 16 | return env._tut_mgr 17 | 18 | def __init__(self): 19 | self.reset() 20 | 21 | def reset(self): 22 | self.tuts = {} 23 | self._options = {} 24 | 25 | self.DEFAULT_PATH = None 26 | self.RESET_PATHS = {} 27 | 28 | @property 29 | def reset_paths(self): 30 | return { 31 | t: path 32 | for t, path in self.tuts.items() 33 | } 34 | 35 | def tut(self, path): 36 | """Return a Tut for the given path.""" 37 | if path not in self.tuts: 38 | self.tuts[path] = Tut(path) 39 | 40 | return self.tuts[path] 41 | 42 | def reset_tuts(self): 43 | for tut in self.tuts.values(): 44 | tut.reset() 45 | 46 | def update_defaults(self, options): 47 | 48 | self._options = options.copy() 49 | 50 | def resolve_option(self, node, key, default=UNSET): 51 | 52 | if key in node.options: 53 | return node.options[key] 54 | 55 | if key in self._options: 56 | return self._options[key] 57 | 58 | if default is not UNSET: 59 | return default 60 | 61 | raise Exception("No tut {0} specified.".format(key)) 62 | 63 | def __getattr__(self, key): 64 | if key.startswith('default_') and key.split('_', 1)[-1] in self._options: 65 | return self._options[key.split('_', 1)[-1]] 66 | 67 | return super(TutManager, self).__getattr__(key) -------------------------------------------------------------------------------- /src/tut/sphinx/checkpoint.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import os 3 | 4 | import sh 5 | from docutils.parsers.rst import Directive, directives 6 | import sphinx.pycode 7 | 8 | from .manager import TutManager 9 | 10 | 11 | class TutDefaults(Directive): 12 | option_spec = { 13 | 'path': directives.path, 14 | 'href': directives.unchanged, 15 | } 16 | 17 | def run(self): 18 | manager = TutManager.get(self.state.document.settings.env) 19 | manager.update_defaults(self.options) 20 | 21 | return [] 22 | 23 | 24 | class TutCheckpoint(Directive): 25 | 26 | has_content = False 27 | required_arguments = 1 28 | optional_arguments = 0 29 | final_argument_whitespace = True 30 | option_spec = { 31 | 'path': directives.path, 32 | } 33 | 34 | def run(self): 35 | manager = TutManager.get(self.state.document.settings.env) 36 | path = manager.resolve_option(self, 'path') 37 | 38 | # paths are relative to the project root 39 | rel_path, tut_path = self.state.document.settings.env.relfn2path(path) 40 | git_ref = self.arguments[0].strip().lower() 41 | 42 | try: 43 | manager.tut(tut_path).checkout(git_ref) 44 | self.state.document.git_ref = git_ref 45 | 46 | except sh.ErrorReturnCode_1 as git_error: 47 | if ("error: pathspec '%s' did not match any file(s) known to git.\n" % (git_ref,)).encode() == git_error.stderr: 48 | raise ValueError( 49 | "git checkpoint '%s' does not exist." % (git_ref,) 50 | ) 51 | 52 | finally: 53 | sphinx.pycode.ModuleAnalyzer.cache = {} 54 | 55 | return [] 56 | 57 | 58 | def initialize(app): 59 | 60 | TutManager.get(app.env).reset() 61 | 62 | 63 | def cleanup(app, exception): 64 | 65 | manager = TutManager.get(app.env) 66 | manager.reset_tuts() 67 | -------------------------------------------------------------------------------- /src/tut/cmd.py: -------------------------------------------------------------------------------- 1 | """Tut. 2 | 3 | Usage: 4 | tut init [] 5 | tut start 6 | tut fetch 7 | tut points 8 | tut edit 9 | tut next [--merge] 10 | 11 | Options: 12 | -h --help Show this screen. 13 | --version Show version. 14 | 15 | """ 16 | 17 | from __future__ import absolute_import 18 | import os 19 | import sys 20 | 21 | from docopt import docopt 22 | 23 | import tut 24 | from tut.model import ( 25 | Tut, 26 | TutException, 27 | ) 28 | 29 | 30 | def init(tut, args): 31 | 32 | path = args.get('') 33 | if path is None: 34 | path = os.getcwd() 35 | else: 36 | path = os.path.join(os.getcwd(), path) 37 | 38 | tut_repo = Tut(path) 39 | 40 | if not os.path.exists(os.path.join(path, '.git')): 41 | tut_repo.init() 42 | 43 | 44 | def fetch(tut, args): 45 | 46 | remote = args.get('') 47 | 48 | for point in tut.points(remote): 49 | tut.start( 50 | point.split('/')[-1], 51 | starting_point=point, 52 | ) 53 | 54 | def start(tut, args): 55 | 56 | tut.start(args['']) 57 | 58 | 59 | def points(tut, args): 60 | 61 | for point in tut.points(): 62 | print(point) 63 | 64 | 65 | def edit(tut, args): 66 | 67 | tut.edit( 68 | args[''], 69 | ) 70 | 71 | 72 | def next_step(tut, args): 73 | 74 | tut.next(merge=args.get('--merge')) 75 | 76 | 77 | CMD_MAP = { 78 | 'start': start, 79 | 'init': init, 80 | 'points': points, 81 | 'edit': edit, 82 | 'next': next_step, 83 | 'fetch': fetch, 84 | } 85 | 86 | 87 | def main(): 88 | arguments = docopt(__doc__, version='Tut %s' % tut.version()) 89 | 90 | for cmd in CMD_MAP: 91 | if arguments.get(cmd): 92 | try: 93 | CMD_MAP[cmd]( 94 | Tut(os.getcwd()), 95 | arguments, 96 | ) 97 | except TutException as e: 98 | print("Error: %s" % e) 99 | sys.exit(1) 100 | 101 | break 102 | 103 | 104 | def post_rewrite(): 105 | 106 | tut = Tut(os.getcwd()) 107 | 108 | for line in sys.stdin: 109 | rewrite = line.split() 110 | tut.move_checkpoints(rewrite[0].strip(), rewrite[1].strip()) 111 | 112 | 113 | if __name__ == '__main__': 114 | main() 115 | -------------------------------------------------------------------------------- /src/tut/sphinx/content.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import os 3 | import subprocess 4 | 5 | from docutils import nodes 6 | from docutils.parsers.rst import Directive, directives 7 | import sphinx.pycode 8 | from sphinx.directives.code import dedent_lines 9 | 10 | from .manager import TutManager 11 | 12 | 13 | class TutExec(Directive): 14 | 15 | has_content = True 16 | required_arguments = 0 17 | optional_arguments = 0 18 | option_spec = { 19 | 'path': directives.path, 20 | 'hide-output': directives.flag, 21 | 'hide-commands': directives.flag, 22 | 'prelude': directives.unchanged, 23 | } 24 | # TK: include_commands, include_output, final_prompt, prelude 25 | 26 | def run(self): 27 | manager = TutManager.get(self.state.document.settings.env) 28 | root = self.options.get('path', manager.default_path) 29 | rel_path, root = self.state.document.settings.env.relfn2path(root) 30 | 31 | show_commands = 'hide-commands' not in self.options 32 | show_output = 'hide-output' not in self.options 33 | 34 | output = [] 35 | lines = self.content 36 | 37 | for line in lines: 38 | cmd = subprocess.run( 39 | line.strip().split('$ ', 1)[-1], 40 | cwd=root, shell=True, 41 | stdout=subprocess.PIPE, 42 | stderr=subprocess.STDOUT, 43 | encoding='utf8', 44 | universal_newlines=True, 45 | ) 46 | if show_commands: 47 | output.append(line) 48 | # TK: check stderr 49 | cmd_output = cmd.stdout.strip() 50 | if show_output and cmd_output: 51 | output.append(cmd_output) 52 | 53 | code = '\n'.join(output) 54 | return [ 55 | nodes.literal_block(code, code), 56 | ] 57 | 58 | 59 | class TutContent(Directive): 60 | 61 | has_content = True 62 | required_arguments = 1 63 | optional_arguments = 0 64 | option_spec = { 65 | 'path': directives.path, 66 | } 67 | 68 | def run(self): 69 | manager = TutManager.get(self.state.document.settings.env) 70 | root = self.options.get('path', manager.default_path) 71 | path = os.path.join(root, self.arguments[0]) 72 | rel_path, path = self.state.document.settings.env.relfn2path(path) 73 | 74 | content = '\n'.join(self.content) 75 | open(path, 'w').write(content) 76 | 77 | # TK: Highlighting as in code blocks 78 | return [ 79 | nodes.literal_block(content, content), 80 | ] 81 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2006 Zope Corporation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Bootstrap a buildout-based project 15 | 16 | Simply run this script in a directory containing a buildout.cfg. 17 | The script accepts buildout command-line options, so you can 18 | use the -c option to specify an alternate configuration file. 19 | 20 | $Id: bootstrap.py 102545 2009-08-06 14:49:47Z chrisw $ 21 | """ 22 | 23 | import os, shutil, sys, tempfile, urllib2 24 | from optparse import OptionParser 25 | 26 | tmpeggs = tempfile.mkdtemp() 27 | 28 | is_jython = sys.platform.startswith('java') 29 | 30 | # parsing arguments 31 | parser = OptionParser() 32 | parser.add_option("-v", "--version", dest="version", 33 | help="use a specific zc.buildout version") 34 | parser.add_option("-d", "--distribute", 35 | action="store_true", dest="distribute", default=True, 36 | help="Use Disribute rather than Setuptools.") 37 | 38 | options, args = parser.parse_args() 39 | 40 | if options.version is not None: 41 | VERSION = '==%s' % options.version 42 | else: 43 | VERSION = '' 44 | 45 | USE_DISTRIBUTE = options.distribute 46 | args = args + ['bootstrap'] 47 | 48 | to_reload = False 49 | try: 50 | import pkg_resources 51 | if not hasattr(pkg_resources, '_distribute'): 52 | to_reload = True 53 | raise ImportError 54 | except ImportError: 55 | ez = {} 56 | if USE_DISTRIBUTE: 57 | exec urllib2.urlopen('http://python-distribute.org/distribute_setup.py' 58 | ).read() in ez 59 | ez['use_setuptools'](to_dir=tmpeggs, download_delay=0, no_fake=True) 60 | else: 61 | exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' 62 | ).read() in ez 63 | ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) 64 | 65 | if to_reload: 66 | reload(pkg_resources) 67 | else: 68 | import pkg_resources 69 | 70 | if sys.platform == 'win32': 71 | def quote(c): 72 | if ' ' in c: 73 | return '"%s"' % c # work around spawn lamosity on windows 74 | else: 75 | return c 76 | else: 77 | def quote (c): 78 | return c 79 | 80 | cmd = 'from setuptools.command.easy_install import main; main()' 81 | ws = pkg_resources.working_set 82 | 83 | if USE_DISTRIBUTE: 84 | requirement = 'distribute' 85 | else: 86 | requirement = 'setuptools' 87 | 88 | if is_jython: 89 | import subprocess 90 | 91 | assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', 92 | quote(tmpeggs), 'zc.buildout' + VERSION], 93 | env=dict(os.environ, 94 | PYTHONPATH= 95 | ws.find(pkg_resources.Requirement.parse(requirement)).location 96 | ), 97 | ).wait() == 0 98 | 99 | else: 100 | assert os.spawnle( 101 | os.P_WAIT, sys.executable, quote (sys.executable), 102 | '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION, 103 | dict(os.environ, 104 | PYTHONPATH= 105 | ws.find(pkg_resources.Requirement.parse(requirement)).location 106 | ), 107 | ) == 0 108 | 109 | ws.add_entry(tmpeggs) 110 | ws.require('zc.buildout' + VERSION) 111 | import zc.buildout.buildout 112 | zc.buildout.buildout.main(args) 113 | shutil.rmtree(tmpeggs) 114 | -------------------------------------------------------------------------------- /src/tut/tests/root/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys, os 4 | 5 | sys.path.append(os.path.abspath('.')) 6 | sys.path.append(os.path.abspath('..')) 7 | 8 | extensions = ['tut.sphinx'] 9 | 10 | jsmath_path = 'dummy.js' 11 | 12 | templates_path = ['_templates'] 13 | 14 | master_doc = 'index' 15 | source_suffix = '.txt' 16 | 17 | project = 'Tut ' 18 | copyright = '2013, Nathan Yergler' 19 | # If this is changed, remember to update the versionchanges! 20 | version = '0.6' 21 | release = '0.6alpha1' 22 | today_fmt = '%B %d, %Y' 23 | # unused_docs = [] 24 | exclude_patterns = ['_build', '**/excluded.*'] 25 | keep_warnings = True 26 | pygments_style = 'sphinx' 27 | show_authors = True 28 | 29 | rst_epilog = '.. |subst| replace:: global substitution' 30 | 31 | html_theme = 'default' 32 | html_theme_path = ['.'] 33 | html_theme_options = {} 34 | html_sidebars = {} 35 | ## '**': 'customsb.html', 36 | ## 'contents': ['contentssb.html', 'localtoc.html'] } 37 | html_style = 'default.css' 38 | html_static_path = ['_static',] 39 | html_last_updated_fmt = '%b %d, %Y' 40 | html_context = {'hckey': 'hcval', 'hckey_co': 'wrong_hcval_co'} 41 | 42 | htmlhelp_basename = 'SphinxTestsdoc' 43 | 44 | latex_documents = [ 45 | ('contents', 'SphinxTests.tex', 'Sphinx Tests Documentation', 46 | 'Georg Brandl \\and someone else', 'manual'), 47 | ] 48 | 49 | latex_additional_files = ['svgimg.svg'] 50 | 51 | texinfo_documents = [ 52 | ('contents', 'SphinxTests', 'Sphinx Tests', 53 | 'Georg Brandl \\and someone else', 'Sphinx Testing', 'Miscellaneous'), 54 | ] 55 | 56 | man_pages = [ 57 | ('contents', 'SphinxTests', 'Sphinx Tests Documentation', 58 | 'Georg Brandl and someone else', 1), 59 | ] 60 | 61 | value_from_conf_py = 84 62 | 63 | coverage_c_path = ['special/*.h'] 64 | coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'} 65 | 66 | autosummary_generate = ['autosummary'] 67 | 68 | extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue '), 69 | 'pyurl': ('http://python.org/%s', None)} 70 | 71 | ## # modify tags from conf.py 72 | ## tags.add('confpytag') 73 | 74 | ## # -- linkcode 75 | 76 | ## if 'test_linkcode' in tags: 77 | ## import glob 78 | 79 | ## extensions.remove('sphinx.ext.viewcode') 80 | ## extensions.append('sphinx.ext.linkcode') 81 | 82 | ## exclude_patterns.extend(glob.glob('*.txt') + glob.glob('*/*.txt')) 83 | ## exclude_patterns.remove('contents.txt') 84 | ## exclude_patterns.remove('objects.txt') 85 | 86 | ## def linkcode_resolve(domain, info): 87 | ## if domain == 'py': 88 | ## fn = info['module'].replace('.', '/') 89 | ## return "http://foobar/source/%s.py" % fn 90 | ## elif domain == "js": 91 | ## return "http://foobar/js/" + info['fullname'] 92 | ## elif domain in ("c", "cpp"): 93 | ## return "http://foobar/%s/%s" % (domain, "".join(info['names'])) 94 | ## else: 95 | ## raise AssertionError() 96 | 97 | ## # -- extension API 98 | 99 | ## from docutils import nodes 100 | ## from sphinx import addnodes 101 | ## from sphinx.util.compat import Directive 102 | 103 | ## def userdesc_parse(env, sig, signode): 104 | ## x, y = sig.split(':') 105 | ## signode += addnodes.desc_name(x, x) 106 | ## signode += addnodes.desc_parameterlist() 107 | ## signode[-1] += addnodes.desc_parameter(y, y) 108 | ## return x 109 | 110 | ## def functional_directive(name, arguments, options, content, lineno, 111 | ## content_offset, block_text, state, state_machine): 112 | ## return [nodes.strong(text='from function: %s' % options['opt'])] 113 | 114 | ## class ClassDirective(Directive): 115 | ## option_spec = {'opt': lambda x: x} 116 | ## def run(self): 117 | ## return [nodes.strong(text='from class: %s' % self.options['opt'])] 118 | 119 | ## def setup(app): 120 | ## app.add_config_value('value_from_conf_py', 42, False) 121 | ## app.add_directive('funcdir', functional_directive, opt=lambda x: x) 122 | ## app.add_directive('clsdir', ClassDirective) 123 | ## app.add_object_type('userdesc', 'userdescrole', '%s (userdesc)', 124 | ## userdesc_parse, objname='user desc') 125 | ## app.add_javascript('file://moo.js') 126 | -------------------------------------------------------------------------------- /src/tut/tests/abs_path/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys, os 4 | 5 | sys.path.append(os.path.abspath('.')) 6 | sys.path.append(os.path.abspath('..')) 7 | 8 | extensions = ['tut.sphinx'] 9 | 10 | jsmath_path = 'dummy.js' 11 | 12 | templates_path = ['_templates'] 13 | 14 | master_doc = 'index' 15 | source_suffix = '.txt' 16 | 17 | project = 'Tut ' 18 | copyright = '2013, Nathan Yergler' 19 | # If this is changed, remember to update the versionchanges! 20 | version = '0.6' 21 | release = '0.6alpha1' 22 | today_fmt = '%B %d, %Y' 23 | # unused_docs = [] 24 | exclude_patterns = ['_build', '**/excluded.*'] 25 | keep_warnings = True 26 | pygments_style = 'sphinx' 27 | show_authors = True 28 | 29 | rst_epilog = '.. |subst| replace:: global substitution' 30 | 31 | html_theme = 'default' 32 | html_theme_path = ['.'] 33 | html_theme_options = {} 34 | html_sidebars = {} 35 | ## '**': 'customsb.html', 36 | ## 'contents': ['contentssb.html', 'localtoc.html'] } 37 | html_style = 'default.css' 38 | html_static_path = ['_static',] 39 | html_last_updated_fmt = '%b %d, %Y' 40 | html_context = {'hckey': 'hcval', 'hckey_co': 'wrong_hcval_co'} 41 | 42 | htmlhelp_basename = 'SphinxTestsdoc' 43 | 44 | latex_documents = [ 45 | ('contents', 'SphinxTests.tex', 'Sphinx Tests Documentation', 46 | 'Georg Brandl \\and someone else', 'manual'), 47 | ] 48 | 49 | latex_additional_files = ['svgimg.svg'] 50 | 51 | texinfo_documents = [ 52 | ('contents', 'SphinxTests', 'Sphinx Tests', 53 | 'Georg Brandl \\and someone else', 'Sphinx Testing', 'Miscellaneous'), 54 | ] 55 | 56 | man_pages = [ 57 | ('contents', 'SphinxTests', 'Sphinx Tests Documentation', 58 | 'Georg Brandl and someone else', 1), 59 | ] 60 | 61 | value_from_conf_py = 84 62 | 63 | coverage_c_path = ['special/*.h'] 64 | coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'} 65 | 66 | autosummary_generate = ['autosummary'] 67 | 68 | extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue '), 69 | 'pyurl': ('http://python.org/%s', None)} 70 | 71 | ## # modify tags from conf.py 72 | ## tags.add('confpytag') 73 | 74 | ## # -- linkcode 75 | 76 | ## if 'test_linkcode' in tags: 77 | ## import glob 78 | 79 | ## extensions.remove('sphinx.ext.viewcode') 80 | ## extensions.append('sphinx.ext.linkcode') 81 | 82 | ## exclude_patterns.extend(glob.glob('*.txt') + glob.glob('*/*.txt')) 83 | ## exclude_patterns.remove('contents.txt') 84 | ## exclude_patterns.remove('objects.txt') 85 | 86 | ## def linkcode_resolve(domain, info): 87 | ## if domain == 'py': 88 | ## fn = info['module'].replace('.', '/') 89 | ## return "http://foobar/source/%s.py" % fn 90 | ## elif domain == "js": 91 | ## return "http://foobar/js/" + info['fullname'] 92 | ## elif domain in ("c", "cpp"): 93 | ## return "http://foobar/%s/%s" % (domain, "".join(info['names'])) 94 | ## else: 95 | ## raise AssertionError() 96 | 97 | ## # -- extension API 98 | 99 | ## from docutils import nodes 100 | ## from sphinx import addnodes 101 | ## from sphinx.util.compat import Directive 102 | 103 | ## def userdesc_parse(env, sig, signode): 104 | ## x, y = sig.split(':') 105 | ## signode += addnodes.desc_name(x, x) 106 | ## signode += addnodes.desc_parameterlist() 107 | ## signode[-1] += addnodes.desc_parameter(y, y) 108 | ## return x 109 | 110 | ## def functional_directive(name, arguments, options, content, lineno, 111 | ## content_offset, block_text, state, state_machine): 112 | ## return [nodes.strong(text='from function: %s' % options['opt'])] 113 | 114 | ## class ClassDirective(Directive): 115 | ## option_spec = {'opt': lambda x: x} 116 | ## def run(self): 117 | ## return [nodes.strong(text='from class: %s' % self.options['opt'])] 118 | 119 | ## def setup(app): 120 | ## app.add_config_value('value_from_conf_py', 42, False) 121 | ## app.add_directive('funcdir', functional_directive, opt=lambda x: x) 122 | ## app.add_directive('clsdir', ClassDirective) 123 | ## app.add_object_type('userdesc', 'userdescrole', '%s (userdesc)', 124 | ## userdesc_parse, objname='user desc') 125 | ## app.add_javascript('file://moo.js') 126 | -------------------------------------------------------------------------------- /src/tut/tests/literalinclude/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys, os 4 | 5 | sys.path.append(os.path.abspath('.')) 6 | sys.path.append(os.path.abspath('..')) 7 | 8 | extensions = ['tut.sphinx'] 9 | 10 | jsmath_path = 'dummy.js' 11 | 12 | templates_path = ['_templates'] 13 | 14 | master_doc = 'index' 15 | source_suffix = '.txt' 16 | 17 | project = 'Tut ' 18 | copyright = '2013, Nathan Yergler' 19 | # If this is changed, remember to update the versionchanges! 20 | version = '0.6' 21 | release = '0.6alpha1' 22 | today_fmt = '%B %d, %Y' 23 | # unused_docs = [] 24 | exclude_patterns = ['_build', '**/excluded.*'] 25 | keep_warnings = True 26 | pygments_style = 'sphinx' 27 | show_authors = True 28 | 29 | rst_epilog = '.. |subst| replace:: global substitution' 30 | 31 | html_theme = 'default' 32 | html_theme_path = ['.'] 33 | html_theme_options = {} 34 | html_sidebars = {} 35 | ## '**': 'customsb.html', 36 | ## 'contents': ['contentssb.html', 'localtoc.html'] } 37 | html_style = 'default.css' 38 | html_static_path = ['_static',] 39 | html_last_updated_fmt = '%b %d, %Y' 40 | html_context = {'hckey': 'hcval', 'hckey_co': 'wrong_hcval_co'} 41 | 42 | htmlhelp_basename = 'SphinxTestsdoc' 43 | 44 | latex_documents = [ 45 | ('contents', 'SphinxTests.tex', 'Sphinx Tests Documentation', 46 | 'Georg Brandl \\and someone else', 'manual'), 47 | ] 48 | 49 | latex_additional_files = ['svgimg.svg'] 50 | 51 | texinfo_documents = [ 52 | ('contents', 'SphinxTests', 'Sphinx Tests', 53 | 'Georg Brandl \\and someone else', 'Sphinx Testing', 'Miscellaneous'), 54 | ] 55 | 56 | man_pages = [ 57 | ('contents', 'SphinxTests', 'Sphinx Tests Documentation', 58 | 'Georg Brandl and someone else', 1), 59 | ] 60 | 61 | value_from_conf_py = 84 62 | 63 | coverage_c_path = ['special/*.h'] 64 | coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'} 65 | 66 | autosummary_generate = ['autosummary'] 67 | 68 | extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue '), 69 | 'pyurl': ('http://python.org/%s', None)} 70 | 71 | ## # modify tags from conf.py 72 | ## tags.add('confpytag') 73 | 74 | ## # -- linkcode 75 | 76 | ## if 'test_linkcode' in tags: 77 | ## import glob 78 | 79 | ## extensions.remove('sphinx.ext.viewcode') 80 | ## extensions.append('sphinx.ext.linkcode') 81 | 82 | ## exclude_patterns.extend(glob.glob('*.txt') + glob.glob('*/*.txt')) 83 | ## exclude_patterns.remove('contents.txt') 84 | ## exclude_patterns.remove('objects.txt') 85 | 86 | ## def linkcode_resolve(domain, info): 87 | ## if domain == 'py': 88 | ## fn = info['module'].replace('.', '/') 89 | ## return "http://foobar/source/%s.py" % fn 90 | ## elif domain == "js": 91 | ## return "http://foobar/js/" + info['fullname'] 92 | ## elif domain in ("c", "cpp"): 93 | ## return "http://foobar/%s/%s" % (domain, "".join(info['names'])) 94 | ## else: 95 | ## raise AssertionError() 96 | 97 | ## # -- extension API 98 | 99 | ## from docutils import nodes 100 | ## from sphinx import addnodes 101 | ## from sphinx.util.compat import Directive 102 | 103 | ## def userdesc_parse(env, sig, signode): 104 | ## x, y = sig.split(':') 105 | ## signode += addnodes.desc_name(x, x) 106 | ## signode += addnodes.desc_parameterlist() 107 | ## signode[-1] += addnodes.desc_parameter(y, y) 108 | ## return x 109 | 110 | ## def functional_directive(name, arguments, options, content, lineno, 111 | ## content_offset, block_text, state, state_machine): 112 | ## return [nodes.strong(text='from function: %s' % options['opt'])] 113 | 114 | ## class ClassDirective(Directive): 115 | ## option_spec = {'opt': lambda x: x} 116 | ## def run(self): 117 | ## return [nodes.strong(text='from class: %s' % self.options['opt'])] 118 | 119 | ## def setup(app): 120 | ## app.add_config_value('value_from_conf_py', 42, False) 121 | ## app.add_directive('funcdir', functional_directive, opt=lambda x: x) 122 | ## app.add_directive('clsdir', ClassDirective) 123 | ## app.add_object_type('userdesc', 'userdescrole', '%s (userdesc)', 124 | ## userdesc_parse, objname='user desc') 125 | ## app.add_javascript('file://moo.js') 126 | -------------------------------------------------------------------------------- /src/tut/tests/relative_path/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys, os 4 | 5 | sys.path.append(os.path.abspath('.')) 6 | sys.path.append(os.path.abspath('..')) 7 | 8 | extensions = ['tut.sphinx'] 9 | 10 | jsmath_path = 'dummy.js' 11 | 12 | templates_path = ['_templates'] 13 | 14 | master_doc = 'index' 15 | source_suffix = '.txt' 16 | 17 | project = 'Tut ' 18 | copyright = '2013, Nathan Yergler' 19 | # If this is changed, remember to update the versionchanges! 20 | version = '0.6' 21 | release = '0.6alpha1' 22 | today_fmt = '%B %d, %Y' 23 | # unused_docs = [] 24 | exclude_patterns = ['_build', '**/excluded.*'] 25 | keep_warnings = True 26 | pygments_style = 'sphinx' 27 | show_authors = True 28 | 29 | rst_epilog = '.. |subst| replace:: global substitution' 30 | 31 | html_theme = 'default' 32 | html_theme_path = ['.'] 33 | html_theme_options = {} 34 | html_sidebars = {} 35 | ## '**': 'customsb.html', 36 | ## 'contents': ['contentssb.html', 'localtoc.html'] } 37 | html_style = 'default.css' 38 | html_static_path = ['_static',] 39 | html_last_updated_fmt = '%b %d, %Y' 40 | html_context = {'hckey': 'hcval', 'hckey_co': 'wrong_hcval_co'} 41 | 42 | htmlhelp_basename = 'SphinxTestsdoc' 43 | 44 | latex_documents = [ 45 | ('contents', 'SphinxTests.tex', 'Sphinx Tests Documentation', 46 | 'Georg Brandl \\and someone else', 'manual'), 47 | ] 48 | 49 | latex_additional_files = ['svgimg.svg'] 50 | 51 | texinfo_documents = [ 52 | ('contents', 'SphinxTests', 'Sphinx Tests', 53 | 'Georg Brandl \\and someone else', 'Sphinx Testing', 'Miscellaneous'), 54 | ] 55 | 56 | man_pages = [ 57 | ('contents', 'SphinxTests', 'Sphinx Tests Documentation', 58 | 'Georg Brandl and someone else', 1), 59 | ] 60 | 61 | value_from_conf_py = 84 62 | 63 | coverage_c_path = ['special/*.h'] 64 | coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'} 65 | 66 | autosummary_generate = ['autosummary'] 67 | 68 | extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue '), 69 | 'pyurl': ('http://python.org/%s', None)} 70 | 71 | ## # modify tags from conf.py 72 | ## tags.add('confpytag') 73 | 74 | ## # -- linkcode 75 | 76 | ## if 'test_linkcode' in tags: 77 | ## import glob 78 | 79 | ## extensions.remove('sphinx.ext.viewcode') 80 | ## extensions.append('sphinx.ext.linkcode') 81 | 82 | ## exclude_patterns.extend(glob.glob('*.txt') + glob.glob('*/*.txt')) 83 | ## exclude_patterns.remove('contents.txt') 84 | ## exclude_patterns.remove('objects.txt') 85 | 86 | ## def linkcode_resolve(domain, info): 87 | ## if domain == 'py': 88 | ## fn = info['module'].replace('.', '/') 89 | ## return "http://foobar/source/%s.py" % fn 90 | ## elif domain == "js": 91 | ## return "http://foobar/js/" + info['fullname'] 92 | ## elif domain in ("c", "cpp"): 93 | ## return "http://foobar/%s/%s" % (domain, "".join(info['names'])) 94 | ## else: 95 | ## raise AssertionError() 96 | 97 | ## # -- extension API 98 | 99 | ## from docutils import nodes 100 | ## from sphinx import addnodes 101 | ## from sphinx.util.compat import Directive 102 | 103 | ## def userdesc_parse(env, sig, signode): 104 | ## x, y = sig.split(':') 105 | ## signode += addnodes.desc_name(x, x) 106 | ## signode += addnodes.desc_parameterlist() 107 | ## signode[-1] += addnodes.desc_parameter(y, y) 108 | ## return x 109 | 110 | ## def functional_directive(name, arguments, options, content, lineno, 111 | ## content_offset, block_text, state, state_machine): 112 | ## return [nodes.strong(text='from function: %s' % options['opt'])] 113 | 114 | ## class ClassDirective(Directive): 115 | ## option_spec = {'opt': lambda x: x} 116 | ## def run(self): 117 | ## return [nodes.strong(text='from class: %s' % self.options['opt'])] 118 | 119 | ## def setup(app): 120 | ## app.add_config_value('value_from_conf_py', 42, False) 121 | ## app.add_directive('funcdir', functional_directive, opt=lambda x: x) 122 | ## app.add_directive('clsdir', ClassDirective) 123 | ## app.add_object_type('userdesc', 'userdescrole', '%s (userdesc)', 124 | ## userdesc_parse, objname='user desc') 125 | ## app.add_javascript('file://moo.js') 126 | -------------------------------------------------------------------------------- /src/tut/tests/tut_directive/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys, os 4 | 5 | sys.path.append(os.path.abspath('.')) 6 | sys.path.append(os.path.abspath('..')) 7 | 8 | extensions = ['tut.sphinx'] 9 | 10 | jsmath_path = 'dummy.js' 11 | 12 | templates_path = ['_templates'] 13 | 14 | master_doc = 'index' 15 | source_suffix = '.txt' 16 | 17 | project = 'Tut ' 18 | copyright = '2013, Nathan Yergler' 19 | # If this is changed, remember to update the versionchanges! 20 | version = '0.6' 21 | release = '0.6alpha1' 22 | today_fmt = '%B %d, %Y' 23 | # unused_docs = [] 24 | exclude_patterns = ['_build', '**/excluded.*'] 25 | keep_warnings = True 26 | pygments_style = 'sphinx' 27 | show_authors = True 28 | 29 | rst_epilog = '.. |subst| replace:: global substitution' 30 | 31 | html_theme = 'default' 32 | html_theme_path = ['.'] 33 | html_theme_options = {} 34 | html_sidebars = {} 35 | ## '**': 'customsb.html', 36 | ## 'contents': ['contentssb.html', 'localtoc.html'] } 37 | html_style = 'default.css' 38 | html_static_path = ['_static',] 39 | html_last_updated_fmt = '%b %d, %Y' 40 | html_context = {'hckey': 'hcval', 'hckey_co': 'wrong_hcval_co'} 41 | 42 | htmlhelp_basename = 'SphinxTestsdoc' 43 | 44 | latex_documents = [ 45 | ('contents', 'SphinxTests.tex', 'Sphinx Tests Documentation', 46 | 'Georg Brandl \\and someone else', 'manual'), 47 | ] 48 | 49 | latex_additional_files = ['svgimg.svg'] 50 | 51 | texinfo_documents = [ 52 | ('contents', 'SphinxTests', 'Sphinx Tests', 53 | 'Georg Brandl \\and someone else', 'Sphinx Testing', 'Miscellaneous'), 54 | ] 55 | 56 | man_pages = [ 57 | ('contents', 'SphinxTests', 'Sphinx Tests Documentation', 58 | 'Georg Brandl and someone else', 1), 59 | ] 60 | 61 | value_from_conf_py = 84 62 | 63 | coverage_c_path = ['special/*.h'] 64 | coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'} 65 | 66 | autosummary_generate = ['autosummary'] 67 | 68 | extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue '), 69 | 'pyurl': ('http://python.org/%s', None)} 70 | 71 | ## # modify tags from conf.py 72 | ## tags.add('confpytag') 73 | 74 | ## # -- linkcode 75 | 76 | ## if 'test_linkcode' in tags: 77 | ## import glob 78 | 79 | ## extensions.remove('sphinx.ext.viewcode') 80 | ## extensions.append('sphinx.ext.linkcode') 81 | 82 | ## exclude_patterns.extend(glob.glob('*.txt') + glob.glob('*/*.txt')) 83 | ## exclude_patterns.remove('contents.txt') 84 | ## exclude_patterns.remove('objects.txt') 85 | 86 | ## def linkcode_resolve(domain, info): 87 | ## if domain == 'py': 88 | ## fn = info['module'].replace('.', '/') 89 | ## return "http://foobar/source/%s.py" % fn 90 | ## elif domain == "js": 91 | ## return "http://foobar/js/" + info['fullname'] 92 | ## elif domain in ("c", "cpp"): 93 | ## return "http://foobar/%s/%s" % (domain, "".join(info['names'])) 94 | ## else: 95 | ## raise AssertionError() 96 | 97 | ## # -- extension API 98 | 99 | ## from docutils import nodes 100 | ## from sphinx import addnodes 101 | ## from sphinx.util.compat import Directive 102 | 103 | ## def userdesc_parse(env, sig, signode): 104 | ## x, y = sig.split(':') 105 | ## signode += addnodes.desc_name(x, x) 106 | ## signode += addnodes.desc_parameterlist() 107 | ## signode[-1] += addnodes.desc_parameter(y, y) 108 | ## return x 109 | 110 | ## def functional_directive(name, arguments, options, content, lineno, 111 | ## content_offset, block_text, state, state_machine): 112 | ## return [nodes.strong(text='from function: %s' % options['opt'])] 113 | 114 | ## class ClassDirective(Directive): 115 | ## option_spec = {'opt': lambda x: x} 116 | ## def run(self): 117 | ## return [nodes.strong(text='from class: %s' % self.options['opt'])] 118 | 119 | ## def setup(app): 120 | ## app.add_config_value('value_from_conf_py', 42, False) 121 | ## app.add_directive('funcdir', functional_directive, opt=lambda x: x) 122 | ## app.add_directive('clsdir', ClassDirective) 123 | ## app.add_object_type('userdesc', 'userdescrole', '%s (userdesc)', 124 | ## userdesc_parse, objname='user desc') 125 | ## app.add_javascript('file://moo.js') 126 | -------------------------------------------------------------------------------- /src/tut/tests/test_sphinx.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | from unittest.mock import ( 4 | ANY, 5 | patch, 6 | ) 7 | 8 | from munch import munchify 9 | from sphinx_testing import with_app 10 | from sphinx_testing.path import path 11 | 12 | from tut.sphinx.manager import TutManager 13 | import tut.sphinx.checkpoint 14 | 15 | 16 | test_root = path(__file__).parent.joinpath('root').abspath() 17 | 18 | 19 | @patch('tut.model.git', return_value='original_branch') 20 | class SphinxExtensionLifecycleTests(TestCase): 21 | 22 | # the order of these decorators is *important*: cleanup needs to 23 | # be replaced before the Sphinx Test Application is instantiated 24 | @patch('tut.sphinx.cleanup') 25 | @with_app(srcdir=test_root) 26 | def test_cleanup_called(self, cleanup_mock, git_mock, sphinx_app, status, warning): 27 | 28 | sphinx_app.build(force_all=True) 29 | 30 | self.assertEqual(cleanup_mock.call_count, 1) 31 | 32 | @with_app(srcdir=test_root) 33 | def test_cleanup_resets_paths(self, git_mock, sphinx_app, status, warning): 34 | 35 | start_dir = os.getcwd() 36 | 37 | sphinx_app.build() 38 | 39 | self.assertEqual(git_mock.call_count, 3) 40 | self.assertEqual(git_mock.call_args_list[1][0], 41 | (ANY, ANY, 'checkout', 'step_one',)) 42 | self.assertEqual(git_mock.call_args_list[2][0], 43 | (ANY, ANY, 'checkout', 'original_branch',)) 44 | 45 | self.assertEqual(start_dir, os.getcwd()) 46 | 47 | 48 | @patch('tut.model.git', return_value='x') 49 | class TutDirectiveTests(TestCase): 50 | 51 | @with_app(srcdir=test_root.parent/'tut_directive') 52 | def test_set_default_path(self, git_mock, sphinx_app, status, warning): 53 | """tut directive sets the default repo path.""" 54 | 55 | sphinx_app.builder.build_all() 56 | 57 | # check the resolved paths in the cache 58 | self.assertTrue( 59 | os.path.join( 60 | os.path.dirname(__file__), 'tut_directive', 'src' 61 | ) in TutManager.get(sphinx_app.env).reset_paths 62 | ) 63 | self.assertEqual(TutManager.get(sphinx_app.env).default_path, '/src') 64 | 65 | 66 | @patch('tut.model.git', return_value='x') 67 | class CheckpointDirectiveTests(TestCase): 68 | 69 | @with_app(srcdir=test_root) 70 | def test_checkpoint_triggers_checkout(self, git_mock, sphinx_app, status, warning): 71 | sphinx_app.build() 72 | 73 | self.assertEqual(git_mock.call_count, 3) 74 | self.assertEqual(git_mock.call_args_list[1][0], 75 | (ANY, ANY, 'checkout', 'step_one',)) 76 | 77 | @with_app(srcdir=test_root) 78 | def test_checkpoint_resets_pycode_cache(self, git_mock, sphinx_app, status, warning): 79 | 80 | # dirty the cache 81 | import sphinx.pycode 82 | sphinx.pycode.ModuleAnalyzer.cache = {'foo': 'bar'} 83 | 84 | # build the project, which includes a checkout directive 85 | sphinx_app.build() 86 | 87 | self.assertEqual(sphinx.pycode.ModuleAnalyzer.cache, {}) 88 | 89 | @with_app(srcdir=test_root.parent/'relative_path') 90 | def test_relative_paths(self, git_mock, sphinx_app, status, warning): 91 | """Paths are relative to the document location.""" 92 | 93 | sphinx_app.builder.build_all() 94 | 95 | # check the resolved paths in the cache 96 | self.assertTrue( 97 | os.path.join( 98 | os.path.dirname(__file__), 'relative_path', 'src' 99 | ) in TutManager.get(sphinx_app.env).reset_paths 100 | ) 101 | 102 | @with_app(srcdir=test_root.parent/'abs_path') 103 | def test_absolute_paths(self, git_mock, sphinx_app, status, warning): 104 | """Absolute paths are relative to the project root.""" 105 | 106 | sphinx_app.builder.build_all() 107 | 108 | # check the resolved paths in the cache 109 | self.assertTrue( 110 | os.path.join( 111 | os.path.dirname(__file__), 'abs_path', 'src' 112 | ) in TutManager.get(sphinx_app.env).reset_paths 113 | ) 114 | 115 | 116 | class CheckpointDirectiveWithLiveGitTests(TestCase): 117 | 118 | def test_invalid_checkpoint(self): 119 | """Invalid checkpoint names return helpful error message.""" 120 | 121 | node = tut.sphinx.checkpoint.TutCheckpoint( 122 | 'checkpoint', 123 | ('blarf',), 124 | {'path': os.getcwd()}, 125 | content='', 126 | lineno=0, 127 | content_offset=0, 128 | block_text=None, 129 | state=munchify({ 130 | 'document': { 131 | 'settings': { 132 | 'env': { 133 | 'relfn2path': lambda p: (p, p) 134 | }, 135 | }, 136 | }, 137 | }), 138 | state_machine=None, 139 | ) 140 | 141 | with self.assertRaises(ValueError) as git_error: 142 | node.run() 143 | 144 | self.assertEqual( 145 | str(git_error.exception), 146 | "git checkpoint 'blarf' does not exist.", 147 | ) 148 | -------------------------------------------------------------------------------- /src/tut/model.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import os 3 | 4 | from sh import git 5 | import yaml 6 | 7 | 8 | DEFAULT_CONFIG = { 9 | 'points': [], 10 | } 11 | 12 | 13 | class TutException(Exception): 14 | pass 15 | 16 | 17 | class Tut(object): 18 | 19 | def __init__(self, path): 20 | self.path = path 21 | 22 | # record the current branch 23 | self._initial_rev = None 24 | try: 25 | self._initial_rev = self._current_branch() 26 | except Exception as e: 27 | pass 28 | 29 | def _git(self, *args, **kwargs): 30 | 31 | return git( 32 | '--git-dir={0}'.format(os.path.join(self.path, '.git')), 33 | '--work-tree={0}'.format(self.path), 34 | *args, **kwargs 35 | ) 36 | 37 | def _repo_dirty(self): 38 | """Return True if the repository is dirty.""" 39 | 40 | UNCHANGED_STATUSES = (' ', '?', '!') 41 | 42 | for status_line in self._git('status', porcelain=True): 43 | if not(status_line[0] in UNCHANGED_STATUSES and 44 | status_line[1] in UNCHANGED_STATUSES): 45 | return True 46 | 47 | return False 48 | 49 | def _config(self): 50 | return yaml.load(self.file('tut', 'tut.cfg')) 51 | 52 | def _update_config(self, config, log=None): 53 | 54 | branch = self._current_branch() 55 | 56 | try: 57 | self._git('checkout', 'tut') 58 | with open(os.path.join(self.path, 'tut.cfg'), 'w') as tut_cfg: 59 | yaml.dump( 60 | config, 61 | tut_cfg, 62 | default_flow_style=False, 63 | ) 64 | self._git('add', 'tut.cfg') 65 | self._git('commit', 66 | m=log or 'Update configuration.', 67 | ) 68 | 69 | finally: 70 | self._git('checkout', branch) 71 | 72 | def init(self): 73 | """Create a new repository with an initial commit.""" 74 | 75 | cwd = os.getcwd() 76 | 77 | # initialize the empty repository 78 | git.init(self.path) 79 | 80 | self._git('commit', 81 | m='Initializing empty Tut project.', 82 | allow_empty=True, 83 | ) 84 | 85 | # create the empty configuration file 86 | self._git('branch', 'tut') 87 | self._update_config( 88 | DEFAULT_CONFIG, 89 | log='Initializing Tut configuration.', 90 | ) 91 | self._git('checkout', 'master') 92 | 93 | def file(self, branch, path): 94 | return self._git('--no-pager', 'show', f'{branch}:{path}').stdout 95 | 96 | def points(self, remote=None): 97 | """Return a list of existing checkpoints (branches). 98 | 99 | The list is returned with the oldest checkpoint first. 100 | 101 | """ 102 | 103 | return self._config()['points'] 104 | 105 | def _current_branch(self): 106 | """Return the current branch of the repo.""" 107 | 108 | return self._git('rev-parse', '--abbrev-ref', 'HEAD').strip() 109 | 110 | def current(self): 111 | """Return the name of the current step.""" 112 | 113 | current_branch = self._current_branch() 114 | 115 | if current_branch in self.points(): 116 | return current_branch 117 | 118 | return None 119 | 120 | def start(self, name, starting_point=None): 121 | """Start a new step (branch).""" 122 | 123 | # make sure this is not a known checkpoint 124 | if name in self.points(): 125 | raise TutException("Duplicate checkpoint.") 126 | 127 | # make sure the repo is clean 128 | if self._repo_dirty(): 129 | raise TutException("Dirty tree.") 130 | 131 | # create the new branch 132 | self._git('branch', name) 133 | 134 | # add the branch to config 135 | config = self._config() 136 | points = config['points'] 137 | if self.current(): 138 | points.insert(points.index(self.current()) + 1, name) 139 | else: 140 | points.append(name) 141 | 142 | self._update_config( 143 | config, 144 | log='Adding new point %s' % name, 145 | ) 146 | 147 | # checkout the new branch 148 | self._git('checkout', name) 149 | 150 | def checkout(self, ref): 151 | self._git('checkout', ref) 152 | 153 | def edit(self, name): 154 | """Start editing the checkpoint point_name.""" 155 | 156 | # make sure this is a known checkpoint 157 | if name not in self.points(): 158 | raise TutException("Unknown checkpoint.") 159 | 160 | # make sure the repo is clean 161 | if self._repo_dirty(): 162 | raise TutException("Dirty tree.") 163 | 164 | self.checkout(name) 165 | 166 | def next(self, merge=False): 167 | current = self.current() 168 | 169 | try: 170 | switch_to = self.points()[ 171 | self.points().index(current) + 1 172 | ] 173 | except IndexError: 174 | # we've reached the end of the list; switch to master 175 | switch_to = 'master' 176 | 177 | self._git('checkout', switch_to) 178 | 179 | if merge: 180 | self._git('merge', current) 181 | 182 | def reset(self): 183 | """Reset the repo to the rev it was at when we started.""" 184 | 185 | if self._initial_rev is not None: 186 | self.checkout(self._initial_rev) 187 | -------------------------------------------------------------------------------- /src/tut/tests/test_model.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | import unittest 5 | from unittest.mock import patch 6 | 7 | from sh import git 8 | 9 | import tut.model 10 | 11 | 12 | class TutTestCase(unittest.TestCase): 13 | 14 | def setUp(self): 15 | 16 | # create a temporary directory to work in 17 | self._testpath = tempfile.mkdtemp() 18 | 19 | # stash the current working directory 20 | self._original_path = os.getcwd() 21 | 22 | def tearDown(self): 23 | 24 | # cleanup working directory 25 | shutil.rmtree(self._testpath) 26 | 27 | # return to the original working directory 28 | os.chdir(self._original_path) 29 | 30 | 31 | class TutInitTests(TutTestCase): 32 | 33 | def test_init_creates_empty_pointfile(self): 34 | 35 | t = tut.model.Tut(self._testpath) 36 | t.init() 37 | 38 | os.chdir(self._testpath) 39 | self.assertEqual( 40 | git('--no-pager', 'show', 'tut:tut.cfg').strip(), 41 | 'points: []', 42 | ) 43 | 44 | def test_tut_can_reset_to_initial_rev(self): 45 | 46 | # create a repository w/ another branch 47 | t = tut.model.Tut(self._testpath) 48 | t.init() 49 | t.start('blarf') 50 | t.checkout('master') 51 | 52 | t = tut.model.Tut(self._testpath) 53 | self.assertEqual(t._current_branch(), 'master') 54 | t.edit('blarf') 55 | self.assertEqual(t.current(), 'blarf') 56 | t.reset() 57 | 58 | self.assertEqual(t._current_branch(), 'master') 59 | 60 | def test_reset_doesnt_fail_when_no_initial_rev(self): 61 | t = tut.model.Tut(self._testpath) 62 | t.init() 63 | t.reset() 64 | 65 | self.assertEqual(t._current_branch(), 'master') 66 | 67 | 68 | class TutPointsTests(TutTestCase): 69 | def test_points_returns_contents_of_pointfile(self): 70 | t = tut.model.Tut(self._testpath) 71 | t.init() 72 | 73 | self.assertEqual(t.points(), []) 74 | 75 | t.start('step1') 76 | t.start('step2') 77 | 78 | self.assertEqual(t.points(), ['step1', 'step2']) 79 | 80 | def test_current(self): 81 | t = tut.model.Tut(self._testpath) 82 | t.init() 83 | 84 | self.assertEqual(t.current(), None) 85 | 86 | t.start('step1') 87 | self.assertEqual(t.current(), 'step1') 88 | 89 | def test_current_returns_none_on_unknown_branch(self): 90 | t = tut.model.Tut(self._testpath) 91 | t.init() 92 | t.start('step1') 93 | 94 | t.checkout('master') 95 | 96 | self.assertNotIn('master', t.points()) 97 | self.assertEqual(t.current(), None) 98 | 99 | 100 | class TutStartEditTests(TutTestCase): 101 | 102 | def test_start_adds_name_to_pointfile(self): 103 | t = tut.model.Tut(self._testpath) 104 | 105 | t.init() 106 | t.start('step1') 107 | 108 | os.chdir(self._testpath) 109 | self.assertEqual( 110 | git('--no-pager', 'show', 'tut:tut.cfg').strip(), 111 | 'points:\n- step1', 112 | ) 113 | 114 | def test_start_raises_exception_on_duplicate_name(self): 115 | t = tut.model.Tut(self._testpath) 116 | 117 | t.init() 118 | t.start('step1') 119 | 120 | with self.assertRaises(tut.model.TutException): 121 | t.start('step1') 122 | 123 | def test_start_inserts_branch_after_current(self): 124 | t = tut.model.Tut(self._testpath) 125 | t.init() 126 | 127 | # create three steps 128 | t.start('step1') 129 | t.start('step2') 130 | t.start('step3') 131 | 132 | t.edit('step2') 133 | 134 | # sanity check 135 | self.assertEqual(t.current(), 'step2') 136 | 137 | # add new step between 2 and 3 138 | t.start('step2a') 139 | 140 | self.assertEqual( 141 | t.points(), 142 | ['step1', 143 | 'step2', 144 | 'step2a', 145 | 'step3', 146 | ], 147 | ) 148 | 149 | def test_start_inserts_branch_after_last_on_master(self): 150 | t = tut.model.Tut(self._testpath) 151 | t.init() 152 | 153 | # create three steps 154 | t.start('step1') 155 | t.start('step2') 156 | t.start('step3') 157 | 158 | t.checkout('master') 159 | 160 | # add new step at the end 161 | t.start('step4') 162 | 163 | self.assertEqual( 164 | t.points(), 165 | ['step1', 166 | 'step2', 167 | 'step3', 168 | 'step4', 169 | ], 170 | ) 171 | 172 | 173 | class TutNextTests(TutTestCase): 174 | 175 | def test_next_checks_out_next_in_list(self): 176 | 177 | t = tut.model.Tut(self._testpath) 178 | t.init() 179 | 180 | # create three steps 181 | t.start('step1') 182 | t.start('step2') 183 | t.start('step3') 184 | 185 | t.edit('step1') 186 | 187 | self.assertEqual(t._current_branch(), 'step1') 188 | 189 | t.next() 190 | self.assertEqual(t._current_branch(), 'step2') 191 | self.assertEqual(t.current(), 'step2') 192 | 193 | 194 | class TutFileTests(TutTestCase): 195 | 196 | def test_file_retrieves_content_from_branch(self): 197 | 198 | t = tut.model.Tut(self._testpath) 199 | t.init() 200 | 201 | os.chdir(self._testpath) 202 | self.assertEqual( 203 | t.file('tut', 'tut.cfg').strip(), 204 | b'points: []', 205 | ) 206 | -------------------------------------------------------------------------------- /src/tut/tests/test_diffs.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | import unittest 3 | 4 | from tut import diff 5 | 6 | 7 | class DiffTests(unittest.TestCase): 8 | 9 | def test_equal_objects_return_empty_string(self): 10 | 11 | self.assertEqual(diff.diff_contents('A', 'A'), '') 12 | 13 | def test_adding_new_toplevel_returns_lines(self): 14 | 15 | previous = textwrap.dedent( 16 | """ 17 | import sys 18 | import os 19 | """, 20 | ) 21 | now = textwrap.dedent( 22 | """ 23 | import sys 24 | import os 25 | import unittest 26 | """, 27 | ) 28 | 29 | self.assertEqual( 30 | diff.diff_contents(previous, now), 31 | 'import unittest\n', 32 | ) 33 | 34 | def test_adding_lines_in_func_returns_new_func(self): 35 | previous = textwrap.dedent( 36 | """ 37 | import sys 38 | 39 | def foo(): 40 | pass 41 | """, 42 | ) 43 | now = textwrap.dedent( 44 | """ 45 | import sys 46 | 47 | def foo(): 48 | return 42 49 | """, 50 | ) 51 | 52 | self.assertEqual( 53 | diff.diff_contents(previous, now), 54 | textwrap.dedent( 55 | """\ 56 | def foo(): 57 | return 42 58 | """) 59 | ) 60 | 61 | def test_removing_lines_in_func_returns_new_func(self): 62 | previous = textwrap.dedent( 63 | """ 64 | import sys 65 | 66 | def foo(): 67 | a = 42 68 | return a 69 | """, 70 | ) 71 | now = textwrap.dedent( 72 | """ 73 | import sys 74 | 75 | def foo(): 76 | return 42 77 | """, 78 | ) 79 | 80 | self.assertEqual( 81 | diff.diff_contents(previous, now), 82 | textwrap.dedent( 83 | """\ 84 | def foo(): 85 | return 42 86 | """) 87 | ) 88 | 89 | def test_ellipsis_between_changes(self): 90 | previous = textwrap.dedent( 91 | """ 92 | import sys 93 | 94 | def foo(): 95 | a = 42 96 | return a 97 | """, 98 | ) 99 | now = textwrap.dedent( 100 | """ 101 | import os 102 | import sys 103 | 104 | def foo(): 105 | return os.getcwd() 106 | """, 107 | ) 108 | 109 | self.assertEqual( 110 | diff.diff_contents(previous, now), 111 | textwrap.dedent( 112 | """\ 113 | import os 114 | 115 | ... 116 | 117 | def foo(): 118 | return os.getcwd() 119 | """) 120 | ) 121 | 122 | def test_add_method_includes_class_header(self): 123 | self.assertEqual( 124 | diff.diff_contents(TEST_CLASS, TEST_CLASS_2, name='adder.py'), 125 | textwrap.dedent("""\ 126 | class Adder(object): 127 | \"""Doc string 128 | \""" 129 | 130 | foo = 42 131 | 132 | ... 133 | 134 | def result(self): 135 | return sum(self._args) 136 | """), 137 | ) 138 | 139 | def test_change_method_includes_class_header(self): 140 | self.assertEqual( 141 | diff.diff_contents(TEST_CLASS_3, TEST_CLASS_2, name='adder.py'), 142 | textwrap.dedent("""\ 143 | class Adder(object): 144 | \"""Doc string 145 | \""" 146 | 147 | foo = 42 148 | 149 | ... 150 | 151 | def result(self): 152 | return sum(self._args) 153 | """), 154 | ) 155 | 156 | def test_top_level_constant_diff(self): 157 | self.assertEqual( 158 | diff.diff_contents(SETTINGS_1, SETTINGS_2, name='settings.py'), 159 | textwrap.dedent("""\ 160 | DATABASES = { 161 | 'default': { 162 | 'ENGINE': 'django.db.backends.sqlite3', 163 | 'NAME': os.path.join(BASE_DIR, 'addresses.sqlite3'), 164 | } 165 | } 166 | """), 167 | ) 168 | 169 | def test_multi_line_import_diff(self): 170 | self.assertEqual( 171 | diff.diff_contents(IMPORT_1, IMPORT_2), 172 | IMPORT_2, 173 | ) 174 | 175 | def test_hightlight_changed_lines_in_context(self): 176 | pass 177 | 178 | def test_strips_blank_lines_post_ellipsis(self): 179 | pass 180 | 181 | 182 | TEST_CLASS = """ 183 | 184 | class Adder(object): 185 | \"""Doc string 186 | \""" 187 | 188 | foo = 42 189 | 190 | def __init__(self, *args): 191 | self._args = args 192 | """ 193 | 194 | TEST_CLASS_2 = """ 195 | 196 | class Adder(object): 197 | \"""Doc string 198 | \""" 199 | 200 | foo = 42 201 | 202 | def __init__(self, *args): 203 | self._args = args 204 | 205 | def result(self): 206 | return sum(self._args) 207 | 208 | """ 209 | 210 | TEST_CLASS_3 = """ 211 | 212 | class Adder(object): 213 | \"""Doc string 214 | \""" 215 | 216 | def __init__(self, *args): 217 | self._args = args 218 | 219 | def result(self): 220 | return self._args[0] + self._args[1] 221 | 222 | """ 223 | 224 | SETTINGS_1 = """ 225 | WSGI_APPLICATION = 'addressbook.wsgi.application' 226 | 227 | # Database 228 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 229 | 230 | DATABASES = { 231 | 'default': { 232 | 'ENGINE': 'django.db.backends.sqlite3', 233 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 234 | } 235 | } 236 | """ 237 | 238 | SETTINGS_2 = """ 239 | WSGI_APPLICATION = 'addressbook.wsgi.application' 240 | 241 | # Database 242 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 243 | 244 | DATABASES = { 245 | 'default': { 246 | 'ENGINE': 'django.db.backends.sqlite3', 247 | 'NAME': os.path.join(BASE_DIR, 'addresses.sqlite3'), 248 | } 249 | } 250 | """ 251 | 252 | IMPORT_1 = """\ 253 | from uuid import ( 254 | uuid4, 255 | ) 256 | """ 257 | 258 | IMPORT_2 = """\ 259 | from uuid import ( 260 | uuid4, 261 | UUID, 262 | ) 263 | import sys 264 | """ 265 | -------------------------------------------------------------------------------- /src/tut/diff.py: -------------------------------------------------------------------------------- 1 | import difflib 2 | 3 | import re 4 | 5 | from sphinx.pycode import ModuleAnalyzer as SphinxModuleAnalyzer 6 | from sphinx.pycode.pgen2 import token 7 | 8 | 9 | emptyline_re = re.compile(r'^\s*(#.*)?$') 10 | 11 | 12 | # Extend Sphinx's Module Analyzer to support top-leve constants 13 | class ModuleAnalyzer(SphinxModuleAnalyzer): 14 | 15 | def find_tags(self): 16 | """Find class, function and method definitions and their location.""" 17 | if self.tags is not None: 18 | return self.tags 19 | self.tokenize() 20 | result = {} 21 | namespace = [] # type: List[unicode] 22 | stack = [] # type: List[Tuple[unicode, unicode, unicode, int]] 23 | indent = 0 24 | decopos = None 25 | defline = False 26 | expect_indent = False 27 | emptylines = 0 28 | 29 | def tokeniter(ignore = (token.COMMENT,)): 30 | for tokentup in self.tokens: 31 | if tokentup[0] not in ignore: 32 | yield tokentup 33 | tokeniter = tokeniter() 34 | for type, tok, spos, epos, line in tokeniter: # type: ignore 35 | if expect_indent and type != token.NL: 36 | if type != token.INDENT: 37 | # no suite -- one-line definition 38 | assert stack 39 | dtype, fullname, startline, _ = stack.pop() 40 | endline = epos[0] 41 | namespace.pop() 42 | result[fullname] = (dtype, startline, endline - emptylines) 43 | expect_indent = False 44 | if tok in ('def', 'class'): 45 | name = next(tokeniter)[1] # type: ignore 46 | namespace.append(name) 47 | fullname = '.'.join(namespace) 48 | stack.append((tok, fullname, decopos or spos[0], indent)) 49 | defline = True 50 | decopos = None 51 | elif type == token.NAME and spos[1] == 0: 52 | name = fullname = tok 53 | namespace.append(name) 54 | stack.append((tok, fullname, decopos or spos[0], indent)) 55 | defline = True 56 | decopos = None 57 | elif type == token.OP and tok == '@': 58 | if decopos is None: 59 | decopos = spos[0] 60 | elif type == token.INDENT: 61 | expect_indent = False 62 | indent += 1 63 | elif type == token.DEDENT: 64 | indent -= 1 65 | # if the stacklevel is the same as it was before the last 66 | # def/class block, this dedent closes that block 67 | if stack and indent == stack[-1][3]: 68 | dtype, fullname, startline, _ = stack.pop() 69 | endline = spos[0] 70 | namespace.pop() 71 | result[fullname] = (dtype, startline, endline - emptylines) 72 | elif type == token.NEWLINE: 73 | # if this line contained a definition, expect an INDENT 74 | # to start the suite; if there is no such INDENT 75 | # it's a one-line definition 76 | if defline: 77 | defline = False 78 | expect_indent = True 79 | emptylines = 0 80 | elif type == token.NL: 81 | # count up if line is empty or comment only 82 | if emptyline_re.match(line): 83 | emptylines += 1 84 | else: 85 | emptylines = 0 86 | self.tags = result 87 | return result 88 | 89 | 90 | def _find_object(codetext, pos_range, name): 91 | """Find the Python object at pos_range needed for diff context. 92 | 93 | codetext is an array of lines 94 | pos_range is a two-tuple: (start, end) 95 | name is a string which may be used to identify the object 96 | 97 | """ 98 | lineno = pos_range[0] 99 | 100 | code = ModuleAnalyzer.for_string(''.join(codetext), name) 101 | tags = code.find_tags() 102 | 103 | # order the tags by starting position and covert to 0 based indices 104 | ordered_tags = sorted( 105 | [[item_type, name, start - 1, end - 1] 106 | for (name, (item_type, start, end)) in tags.items()], 107 | key=lambda t: t[2], 108 | ) 109 | 110 | if not ordered_tags: 111 | return 112 | 113 | # determine the "display end" of each tag 114 | for i in range(len(ordered_tags) - 1): 115 | ordered_tags[i].append(ordered_tags[i+1][2]) 116 | ordered_tags[-1].append(ordered_tags[-1][-1]) 117 | 118 | # find all overlapping segments 119 | segments = [ 120 | (item_type, name, start, end, display_end) 121 | for (item_type, name, start, end, display_end) in ordered_tags 122 | if (lineno >= start and lineno < end) 123 | ] 124 | 125 | # generate (start, end) pairs 126 | for _, _, start, _, end in segments: 127 | yield start, end 128 | 129 | 130 | def _strip_lines(lines): 131 | 132 | if not lines: 133 | return lines 134 | 135 | start = 0 136 | end = len(lines) 137 | 138 | while not(lines[start].strip()) and start < end: 139 | start += 1 140 | 141 | while not(lines[end-1].strip()) and end > start: 142 | end -= 1 143 | 144 | return lines[start:end] 145 | 146 | 147 | def diff_contents(previously, now, name=''): 148 | """Given two versions of code and return the diff needed for documentation.""" 149 | 150 | previously = previously.splitlines(keepends=True) 151 | now = now.splitlines(keepends=True) 152 | 153 | result = [] 154 | ranges = [] 155 | 156 | for group in difflib.SequenceMatcher(None, previously, now).get_grouped_opcodes(n=0): 157 | for tag, i1, i2, j1, j2 in group: 158 | if tag == 'equal': 159 | ## result.extend(now[j1:j2]) 160 | continue 161 | 162 | if tag in ('insert', 'replace'): 163 | # determine if this is an indented block or not 164 | fl_no, first_line = next( 165 | (i, line) for (i, line) in enumerate(now[j1:j2], j1) 166 | if line.rstrip() 167 | ) 168 | 169 | if first_line[0] != ' ': 170 | # this block is unindented, just append it to the result 171 | ranges.append((j1, j2)) 172 | else: 173 | # grab the object from the code analyzer 174 | for start, end in _find_object(now, (fl_no, j2), name=name): 175 | ranges.append((start, end)) 176 | 177 | # fix up overlapping ranges 178 | for i in range(len(ranges) - 1): 179 | # if this range starts before a previous one, fix that 180 | if i > 0 and ranges[i][0] < ranges[i-1][1]: 181 | ranges[i] = (ranges[i-1][1], ranges[i][1]) 182 | 183 | # fetch the lines and add ellipsis 184 | ellipsis = ['\n', '...\n', '\n'] 185 | if not ranges: 186 | return '' 187 | 188 | prev_end = ranges[0][0] 189 | for start, end in ranges: 190 | if start > prev_end: 191 | result.extend(ellipsis) 192 | 193 | result.extend(_strip_lines(now[start:end])) 194 | prev_end = end 195 | 196 | return ''.join(result) 197 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Tut 3 | ===== 4 | 5 | .. image:: https://travis-ci.org/nyergler/tut.svg?branch=master 6 | :target: https://travis-ci.org/nyergler/tut 7 | 8 | .. image:: https://coveralls.io/repos/github/nyergler/tut/badge.svg?branch=master 9 | :target: https://coveralls.io/github/nyergler/tut?branch=master 10 | 11 | 12 | **Tut** is a tool that helps you write technical documentation using Sphinx_ 1.6 and later. 13 | 14 | **Tut** provides a workflow that supports tutorial-style documents particularly well. If your writing includes code samples that build on one another, Tut is for you. **Tut** helps you manage the code in the tutorial as you write it, and include the correct segments in your document. 15 | 16 | **Tut** makes it easy to manage a git_ source repository for your tutorial's code by using branches_ to record different steps. As you write the code for your tutorial, **Tut** allows you to include code from a particular step in your Sphinx document. **Tut** also has basic support for showing the difference between two branches, allowing you to effectively show what's changed in a way that's readable for humans. 17 | 18 | **Tut** consists of two pieces: a program to manage branches, and a Sphinx 19 | extension to switch branches during the Sphinx build. 20 | 21 | 22 | Using Tut 23 | ========= 24 | 25 | I wrote **Tut** because I wanted an easier way to manage the sample code I was writing for `Effective Django`_. I was using ``git`` to track my changes to the text, but those changes weren't the ones I was reflecting in the code: I could use git to tell me what changed in the text between two points in time, but I couldn't easily tell what changed between chapters. The code, in effect, was a parallel set of changes, and I was interested in understanding them over the course of the text, not (necessarily) over the course of my writing timeline. 26 | 27 | **Tut** is a command-line tool that makes managing the code changes independently of the text changes more straight-forward. It allows you to define a set of "points" in the development of your source and switch back and forth between them. If you make a change to an early point in your code, you can roll that change forward so your future code is consistent. Under the hood **Tut** uses ``git``, so you can include your code as a sub-module and use the other git tools you've come to appreciate. 28 | 29 | To start using **Tut**, run ``tut init ``:: 30 | 31 | $ tut init ./demosrc 32 | 33 | If the path (``./demosrc``) is not an existing git repository, **Tut** 34 | will initialize one and add an initial commit. 35 | 36 | Subsequent **Tut** commands should be run from within the **Tut**-managed 37 | repository. 38 | 39 | :: 40 | 41 | $ cd demosrc 42 | 43 | To start a point from your current position, run ``tut start``:: 44 | 45 | $ tut start step_one 46 | 47 | After you've created different points in your repository, you can run ``tut points`` to list them:: 48 | 49 | $ tut points 50 | step_one 51 | step_two 52 | 53 | If you realize you've made a mistake and want to change the code at an 54 | earlier checkpoint, simply run ``tut edit``:: 55 | 56 | $ tut edit step_one 57 | 58 | **Tut** will check out the ``step_one`` branch, and you can make changes and commit them. Once you're done editing, commit your changes using ``git``. You'll also want to roll those changes forward, through the subsequent steps. 59 | 60 | :: 61 | 62 | $ tut next --merge 63 | 64 | Running ``tut next`` will find the next step and check out that 65 | branch. Adding ``--merge`` will also merge the previous step. If we're 66 | done making changes to ``step_one``, running ``tut next --merge`` will 67 | move us to ``step_two`` and merge ``step_one``. 68 | 69 | Including Code in Sphinx 70 | ======================== 71 | 72 | Sphinx provides the literalinclude_ directive, which allows you to 73 | include source files, or parts of files, in your documentation. **Tut** 74 | allows you to switch to a specific git tag, branch, or commit before 75 | processing the inclusion. 76 | 77 | To enable **Tut**, add ``tut.sphinx`` to the list of enabled extensions in 78 | your Sphinx project's ``conf.py`` file:: 79 | 80 | extensions = [ 81 | # ... 82 | 'tut.sphinx', 83 | ] 84 | 85 | The ``checkpoint`` directive takes a single argument, which is the git 86 | reference to switch to. For example, the following directive will 87 | checkout ``step_one`` (either a branch or tag) in the git repository 88 | in ``/src``:: 89 | 90 | .. tut:checkpoint:: step_one 91 | :path: /src 92 | 93 | The directive doesn't result in any output, but ``literalinclude`` (or 94 | other file-system inclusion directives) that come after the 95 | ``checkpoint`` will use the newly checked-out version. 96 | 97 | **Tut** records the starting state of repositories the first time it 98 | does a checkout, and restores the initial state after the build completes. 99 | 100 | If your document contains multiple checkpoints, you can specify the 101 | path once using the ``tut`` directive:: 102 | 103 | .. tut:: 104 | :path: /src 105 | 106 | Note that ``/src`` is evaluated using the same rules as govern 107 | literalinclude_. That is, the file name is usually relative to the 108 | current file’s path. However, if it is absolute (starting with /), it 109 | is relative to the top source directory. 110 | 111 | Within a checkpoint **Tut** provides two new directives for fetching content: ``tut:literalinclude`` and ``tut:diff``. 112 | 113 | ``tut:literalinclude`` works a lot like Sphinx's built-in literalinclude_ directive. However, instead of loading the file from the filesystem directly, ``tut:literalinclude`` retrieves it from the git repository. 114 | 115 | For example:: 116 | 117 | .. tut:checkpoint:: step_two 118 | :path: /src 119 | 120 | ... 121 | 122 | .. tut:literalinclude:: setup.py 123 | 124 | Will fetch ``setup.py`` from the ``step_two`` branch in the git repository located at ``/src``. 125 | 126 | **Tut** can also show the changes between two checkpoints (branches) using the ``tut:diff`` directive. Like ``tut:literalinclude`` it uses the git repository referenced in the last checkpoint by default. You can specify the ``ref`` and ``prev_ref`` to compare; if omitted, ``ref`` defaults to the current checkpoint and ``prev_ref`` defaults to the previous point, as listed in the output of ``tut points``. 127 | 128 | :: 129 | 130 | .. tut:diff:: setup.py 131 | :ref: step_two 132 | :prev_ref: step_one 133 | :path: /src/demosrc 134 | 135 | 136 | N.B. 137 | ==== 138 | 139 | When Sphinx encounters a ``checkpoint`` directive, it performs a ``git 140 | checkout`` in target repository. This means that the repository should 141 | not contain uncommitted changes, to avoid errors on checkout. 142 | 143 | Note that this will probably change soon, to allow for more flexible use of content from the git repository. 144 | 145 | Developing Tut 146 | ============== 147 | 148 | Making a Release 149 | ---------------- 150 | 151 | When you're ready to make a release, bumpversion_ will handle incrementing the version, tagging, and updating NEWS. 152 | 153 | :: 154 | 155 | $ bumpversion 156 | $ git push 157 | $ git push --tags 158 | 159 | 160 | .. _`Effective Django`: http://www.effectivedjango.com/ 161 | .. _Sphinx: http://sphinx-doc.org/ 162 | .. _branches: http://git-scm.com/book/en/Git-Branching-Basic-Branching-and-Merging 163 | .. _git: http://git-scm.org/ 164 | .. _literalinclude: http://sphinx-doc.org/markup/code.html#directive-literalinclude 165 | -------------------------------------------------------------------------------- /src/tut/sphinx/code.py: -------------------------------------------------------------------------------- 1 | from docutils import nodes 2 | from docutils.parsers.rst import Directive, directives 3 | from docutils.statemachine import ViewList 4 | from sphinx.directives.code import ( 5 | dedent_lines, 6 | LiteralInclude, 7 | LiteralIncludeReader as SphinxLiteralIncludeReader, 8 | ) 9 | from sphinx.util.nodes import set_source_info 10 | 11 | from tut import diff 12 | from .manager import TutManager 13 | 14 | 15 | class LiteralIncludeReader(SphinxLiteralIncludeReader): 16 | 17 | def __init__(self, filename, options, config, tut, gitref): 18 | self._tut = tut 19 | self._gitref = gitref 20 | 21 | super().__init__(filename, options, config) 22 | 23 | def read_file(self, filename, location=None): 24 | # type: (unicode, Any) -> List[unicode] 25 | 26 | try: 27 | text = self._tut.file(self._gitref, self.filename[len(self._tut.path) + 1:]).decode(self.encoding) 28 | 29 | if 'tab-width' in self.options: 30 | text = text.expandtabs(self.options['tab-width']) 31 | 32 | lines = text.splitlines(True) 33 | if 'dedent' in self.options: 34 | return dedent_lines(lines, self.options.get('dedent'), location=location) 35 | else: 36 | return lines 37 | except (IOError, OSError): 38 | raise IOError(_('Include file %r not found or reading it failed') % filename) 39 | except UnicodeError: 40 | raise UnicodeError(_('Encoding %r used for reading included file %r seems to ' 41 | 'be wrong, try giving an :encoding: option') % 42 | (self.encoding, filename)) 43 | 44 | class TutLiteralInclude(LiteralInclude): 45 | 46 | def run(self): 47 | # type: () -> List[nodes.Node] 48 | document = self.state.document 49 | if not document.settings.file_insertion_enabled: 50 | return [document.reporter.warning('File insertion disabled', 51 | line=self.lineno)] 52 | env = document.settings.env 53 | 54 | # get the current Tut 55 | manager = TutManager.get(env) 56 | rel_path, tut_path = self.state.document.settings.env.relfn2path( 57 | manager.resolve_option(self, 'path'), 58 | ) 59 | 60 | # convert options['diff'] to absolute path 61 | if 'diff' in self.options: 62 | _, path = env.relfn2path(self.options['diff']) 63 | self.options['diff'] = path 64 | 65 | try: 66 | location = self.state_machine.get_source_and_line(self.lineno) 67 | rel_filename, filename = env.relfn2path(self.arguments[0]) 68 | env.note_dependency(rel_filename) 69 | 70 | reader = LiteralIncludeReader( 71 | filename, self.options, env.config, 72 | tut=manager.tut(tut_path), 73 | gitref=self.state.document.git_ref, 74 | ) 75 | text, lines = reader.read(location=location) 76 | 77 | retnode = nodes.literal_block(text, text, source=filename) 78 | set_source_info(self, retnode) 79 | if self.options.get('diff'): # if diff is set, set udiff 80 | retnode['language'] = 'udiff' 81 | elif 'language' in self.options: 82 | retnode['language'] = self.options['language'] 83 | retnode['linenos'] = ('linenos' in self.options or 84 | 'lineno-start' in self.options or 85 | 'lineno-match' in self.options) 86 | retnode['classes'] += self.options.get('class', []) 87 | extra_args = retnode['highlight_args'] = {} 88 | if 'emphasize-lines' in self.options: 89 | hl_lines = parselinenos(self.options['emphasize-lines'], lines) 90 | if any(i >= lines for i in hl_lines): 91 | logger.warning('line number spec is out of range(1-%d): %r' % 92 | (lines, self.options['emphasize_lines']), 93 | location=location) 94 | extra_args['hl_lines'] = [x + 1 for x in hl_lines if x < lines] 95 | extra_args['linenostart'] = reader.lineno_start 96 | 97 | if 'caption' in self.options: 98 | caption = self.options['caption'] or self.arguments[0] 99 | retnode = container_wrapper(self, retnode, caption) 100 | 101 | # retnode will be note_implicit_target that is linked from caption and numref. 102 | # when options['name'] is provided, it should be primary ID. 103 | self.add_name(retnode) 104 | 105 | return [retnode] 106 | except Exception as exc: 107 | return [document.reporter.warning(str(exc), line=self.lineno)] 108 | 109 | 110 | class TutCodeDiff(Directive): 111 | has_content = False 112 | required_arguments = 1 113 | optional_arguments = 0 114 | final_argument_whitespace = True 115 | option_spec = { 116 | 'path': directives.path, 117 | 'prev_ref': directives.unchanged, 118 | 'ref': directives.unchanged, 119 | } 120 | 121 | def run(self): 122 | manager = TutManager.get(self.state.document.settings.env) 123 | 124 | tut_path = manager.resolve_option(self, 'path') 125 | tut_href = manager.resolve_option(self, 'href', None) 126 | 127 | # paths are relative to the project root 128 | rel_path, tut_path = self.state.document.settings.env.relfn2path( 129 | tut_path) 130 | rel_obj_name, _ = self.state.document.settings.env.relfn2path( 131 | self.arguments[0], 132 | ) 133 | rel_obj_name = rel_obj_name[len(rel_path) + 1:] 134 | 135 | # use the last checkpoint set if ref is not specified 136 | ref = self.options.get('ref', self.state.document.git_ref) 137 | 138 | # use the previous point if prev_ref is not specified 139 | prev_ref = self.options.get('prev_ref') 140 | if not prev_ref: 141 | points = manager.tut(tut_path).points() 142 | prev_ref = points[points.index(ref) - 1] 143 | 144 | new = manager.tut(tut_path).file(ref, rel_obj_name).decode('utf8') 145 | old = manager.tut(tut_path).file(prev_ref, rel_obj_name).decode('utf8') 146 | 147 | code = diff.diff_contents(old, new, name=rel_obj_name) 148 | literal = nodes.literal_block(code, code) 149 | literal['language'] = 'python' 150 | 151 | if tut_href: 152 | link = tut_href.format(checkpoint=ref, path=rel_obj_name) 153 | caption = 'View file `{0} <{2}>`__'.format(rel_obj_name, ref, link) 154 | literal = container_wrapper(self, literal, caption) 155 | 156 | return [literal] 157 | 158 | 159 | def container_wrapper(directive, literal_node, caption): 160 | container_node = nodes.container('', literal_block=True, 161 | classes=['literal-diff-wrapper']) 162 | parsed = nodes.Element() 163 | directive.state.nested_parse(ViewList([caption], source=''), 164 | directive.content_offset, parsed) 165 | if isinstance(parsed[0], nodes.system_message): 166 | raise ValueError(parsed[0]) 167 | caption_node = nodes.caption(parsed[0].rawsource, '', 168 | *parsed[0].children) 169 | caption_node.source = literal_node.source 170 | caption_node.line = literal_node.line 171 | container_node += caption_node 172 | container_node += literal_node 173 | return container_node 174 | --------------------------------------------------------------------------------