├── .gitignore
├── MANIFEST.in
├── bin
└── sphinxtogithub
├── sphinxtogithub
├── tests
│ ├── __init__.py
│ ├── remover.py
│ ├── replacer.py
│ ├── layout.py
│ ├── setup.py
│ ├── renamer.py
│ ├── filehandler.py
│ ├── directoryhandler.py
│ └── layoutfactory.py
├── __init__.py
└── sphinxtogithub.py
├── LICENCE
├── setup.py
└── README.rst
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENCE
2 | include README.rst
3 | recursive-include sphinxtogithub *.py
4 | prune sphinxtogithub/*.pyc
--------------------------------------------------------------------------------
/bin/sphinxtogithub:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 | from sphinxtogithub.sphinxtogithub import main
4 |
5 | if __name__ == "__main__":
6 | main(sys.argv[1:])
--------------------------------------------------------------------------------
/sphinxtogithub/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | class MockExists(object):
3 |
4 | def __call__(self, name):
5 | self.name = name
6 | return True
7 |
8 | class MockRemove(MockExists):
9 |
10 | pass
11 |
12 |
13 | class MockStream(object):
14 |
15 | def __init__(self):
16 | self.msgs = []
17 |
18 | def write(self, msg):
19 | self.msgs.append(msg)
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/sphinxtogithub/tests/remover.py:
--------------------------------------------------------------------------------
1 |
2 | from sphinxtogithub.tests import MockExists, MockRemove
3 |
4 | import sphinxtogithub
5 | import unittest
6 |
7 | class TestRemover(unittest.TestCase):
8 |
9 | def testCall(self):
10 |
11 | exists = MockExists()
12 | remove = MockRemove()
13 | remover = sphinxtogithub.Remover(exists, remove)
14 |
15 | filepath = "filepath"
16 | remover(filepath)
17 |
18 | self.assertEqual(filepath, exists.name)
19 | self.assertEqual(filepath, remove.name)
20 |
21 |
22 | def testSuite():
23 | suite = unittest.TestSuite()
24 |
25 | suite.addTest(TestRemover("testCall"))
26 |
27 | return suite
28 |
29 |
--------------------------------------------------------------------------------
/sphinxtogithub/__init__.py:
--------------------------------------------------------------------------------
1 | """Script for preparing the html output of the Sphinx documentation system for
2 | github pages. """
3 |
4 | VERSION = (1, 1, 0, 'dev')
5 |
6 | __version__ = ".".join(map(str, VERSION[:-1]))
7 | __release__ = ".".join(map(str, VERSION))
8 | __author__ = "Michael Jones"
9 | __contact__ = "http://github.com/michaeljones"
10 | __homepage__ = "http://github.com/michaeljones/sphinx-to-github"
11 | __docformat__ = "restructuredtext"
12 |
13 | from sphinxtogithub import (
14 | setup,
15 | sphinx_extension,
16 | LayoutFactory,
17 | Layout,
18 | DirectoryHandler,
19 | VerboseRename,
20 | ForceRename,
21 | Remover,
22 | FileHandler,
23 | Replacer,
24 | DirHelper,
25 | FileSystemHelper,
26 | OperationsFactory,
27 | HandlerFactory
28 | )
29 |
30 |
--------------------------------------------------------------------------------
/sphinxtogithub/tests/replacer.py:
--------------------------------------------------------------------------------
1 |
2 | import unittest
3 |
4 | import sphinxtogithub
5 |
6 | class TestReplacer(unittest.TestCase):
7 |
8 | before = """
9 |
Breathe's documentation — BreatheExample v0.0.1 documentation
10 |
11 |
12 | """
13 |
14 | after = """
15 | Breathe's documentation — BreatheExample v0.0.1 documentation
16 |
17 |
18 | """
19 |
20 | def testReplace(self):
21 |
22 | replacer = sphinxtogithub.Replacer("_static/default.css", "static/default.css")
23 | self.assertEqual(replacer.process(self.before), self.after)
24 |
25 |
26 | def testSuite():
27 |
28 | suite = unittest.TestSuite()
29 |
30 | suite.addTest(TestReplacer("testReplace"))
31 |
32 | return suite
33 |
34 |
35 |
--------------------------------------------------------------------------------
/sphinxtogithub/tests/layout.py:
--------------------------------------------------------------------------------
1 |
2 | from sphinxtogithub.tests import MockExists, MockRemove
3 |
4 | import sphinxtogithub
5 | import unittest
6 |
7 | class MockHandler(object):
8 |
9 | def __init__(self):
10 |
11 | self.processed = False
12 |
13 | def process(self):
14 |
15 | self.processed = True
16 |
17 |
18 |
19 | class TestLayout(unittest.TestCase):
20 |
21 | def testProcess(self):
22 |
23 | directory_handlers = []
24 | file_handlers = []
25 |
26 | for i in range(0, 10):
27 | directory_handlers.append(MockHandler())
28 | for i in range(0, 5):
29 | file_handlers.append(MockHandler())
30 |
31 | layout = sphinxtogithub.Layout(directory_handlers, file_handlers)
32 |
33 | layout.process()
34 |
35 | # Check all handlers are processed by reducing them with "and"
36 | self.assert_(reduce(lambda x, y: x and y.processed, directory_handlers, True))
37 | self.assert_(reduce(lambda x, y: x and y.processed, file_handlers, True))
38 |
39 |
40 | def testSuite():
41 | suite = unittest.TestSuite()
42 |
43 | suite.addTest(TestLayout("testProcess"))
44 |
45 | return suite
46 |
47 |
--------------------------------------------------------------------------------
/sphinxtogithub/tests/setup.py:
--------------------------------------------------------------------------------
1 |
2 | import sphinxtogithub
3 | import unittest
4 |
5 | class MockApp(object):
6 |
7 | def __init__(self):
8 | self.config_values = {}
9 | self.connections = {}
10 |
11 | def add_config_value(self, name, default, rebuild):
12 |
13 | self.config_values[name] = (default, rebuild)
14 |
15 | def connect(self, stage, function):
16 |
17 | self.connections[stage] = function
18 |
19 |
20 | class TestSetup(unittest.TestCase):
21 |
22 | def testSetup(self):
23 |
24 | # Sadly not flexible enough to test it independently
25 | # so the tests rely on and test the values pass in the
26 | # production code
27 | app = MockApp()
28 | sphinxtogithub.setup(app)
29 |
30 | self.assertEqual(app.connections["build-finished"], sphinxtogithub.sphinx_extension)
31 | self.assertEqual(len(app.connections), 1)
32 |
33 | self.assertEqual(app.config_values["sphinx_to_github"],(True, ''))
34 | self.assertEqual(app.config_values["sphinx_to_github_verbose"],(True, ''))
35 | self.assertEqual(app.config_values["sphinx_to_github_encoding"],('utf-8', ''))
36 | self.assertEqual(len(app.config_values),3)
37 |
38 |
39 | def testSuite():
40 | suite = unittest.TestSuite()
41 |
42 | suite.addTest(TestSetup("testSetup"))
43 |
44 | return suite
45 |
46 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | // BSD licence, modified to remove the organisation as there isn't one.
2 |
3 | Copyright (c) 2009, Michael Jones
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification,
7 | are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice,
10 | this list of conditions and the following disclaimer.
11 | * Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 | * The names of its contributors may not be used to endorse or promote
15 | products derived from this software without specific prior written
16 | permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 |
--------------------------------------------------------------------------------
/sphinxtogithub/tests/renamer.py:
--------------------------------------------------------------------------------
1 |
2 | from sphinxtogithub.tests import MockExists, MockRemove, MockStream
3 |
4 | import sphinxtogithub
5 | import unittest
6 | import os
7 |
8 |
9 |
10 | class MockRename(object):
11 |
12 | def __call__(self, from_, to):
13 | self.from_ = from_
14 | self.to = to
15 |
16 | class TestForceRename(unittest.TestCase):
17 |
18 | def testCall(self):
19 |
20 | rename = MockRename()
21 | remove = MockRemove()
22 | renamer = sphinxtogithub.ForceRename(rename, remove)
23 |
24 | from_ = "from"
25 | to = "to"
26 | renamer(from_, to)
27 |
28 | self.assertEqual(rename.from_, from_)
29 | self.assertEqual(rename.to, to)
30 | self.assertEqual(remove.name, to)
31 |
32 |
33 | class TestVerboseRename(unittest.TestCase):
34 |
35 | def testCall(self):
36 |
37 | rename = MockRename()
38 | stream = MockStream()
39 | renamer = sphinxtogithub.VerboseRename(rename, stream)
40 |
41 | from_ = os.path.join("path", "to", "from")
42 | to = os.path.join("path", "to", "to")
43 | renamer(from_, to)
44 |
45 | self.assertEqual(rename.from_, from_)
46 | self.assertEqual(rename.to, to)
47 | self.assertEqual(
48 | stream.msgs[0],
49 | "Renaming directory '%s' -> '%s'\n" % (os.path.basename(from_), os.path.basename(to))
50 | )
51 |
52 |
53 |
54 | def testSuite():
55 | suite = unittest.TestSuite()
56 |
57 | suite.addTest(TestForceRename("testCall"))
58 | suite.addTest(TestVerboseRename("testCall"))
59 |
60 | return suite
61 |
62 |
--------------------------------------------------------------------------------
/sphinxtogithub/tests/filehandler.py:
--------------------------------------------------------------------------------
1 |
2 | import unittest
3 |
4 | import sphinxtogithub
5 |
6 | class MockFileObject(object):
7 |
8 | before = """
9 | Breathe's documentation — BreatheExample v0.0.1 documentation
10 |
11 |
12 | """
13 |
14 | after = """
15 | Breathe's documentation — BreatheExample v0.0.1 documentation
16 |
17 |
18 | """
19 |
20 | def read(self):
21 |
22 | return self.before
23 |
24 | def write(self, text):
25 |
26 | self.written = text
27 |
28 | class MockOpener(object):
29 |
30 | def __init__(self):
31 |
32 | self.file_object = MockFileObject()
33 |
34 | def __call__(self, name, readmode="r"):
35 |
36 | self.name = name
37 |
38 | return self.file_object
39 |
40 |
41 |
42 | class TestFileHandler(unittest.TestCase):
43 |
44 | def testProcess(self):
45 |
46 | filepath = "filepath"
47 |
48 | opener = MockOpener()
49 | file_handler = sphinxtogithub.FileHandler(filepath, [], opener)
50 |
51 | file_handler.process()
52 |
53 | self.assertEqual(opener.file_object.written, MockFileObject.before)
54 | self.assertEqual(opener.name, filepath)
55 |
56 | def testProcessWithReplacers(self):
57 |
58 | filepath = "filepath"
59 |
60 | replacers = []
61 | replacers.append(sphinxtogithub.Replacer("_static/default.css", "static/default.css"))
62 | replacers.append(sphinxtogithub.Replacer("_static/pygments.css", "static/pygments.css"))
63 |
64 | opener = MockOpener()
65 | file_handler = sphinxtogithub.FileHandler(filepath, replacers, opener)
66 |
67 | file_handler.process()
68 |
69 | self.assertEqual(opener.file_object.written, MockFileObject.after)
70 |
71 |
72 |
73 | def testSuite():
74 | suite = unittest.TestSuite()
75 |
76 | suite.addTest(TestFileHandler("testProcess"))
77 | suite.addTest(TestFileHandler("testProcessWithReplacers"))
78 |
79 | return suite
80 |
81 |
--------------------------------------------------------------------------------
/sphinxtogithub/tests/directoryhandler.py:
--------------------------------------------------------------------------------
1 |
2 | import unittest
3 | import os
4 |
5 | import sphinxtogithub
6 |
7 |
8 | class MockRenamer(object):
9 |
10 | def __call__(self, from_, to):
11 |
12 | self.from_ = from_
13 | self.to = to
14 |
15 | class TestDirectoryHandler(unittest.TestCase):
16 |
17 | def setUp(self):
18 |
19 | self.directory = "_static"
20 | self.new_directory = "static"
21 | self.root = os.path.join("build", "html")
22 | renamer = MockRenamer()
23 | self.dir_handler = sphinxtogithub.DirectoryHandler(self.directory, self.root, renamer)
24 |
25 | def tearDown(self):
26 |
27 | self.dir_handler = None
28 |
29 |
30 | def testPath(self):
31 |
32 | self.assertEqual(self.dir_handler.path(), os.path.join(self.root, self.directory))
33 |
34 | def testRelativePath(self):
35 |
36 | dir_name = "css"
37 | dir_path = os.path.join(self.root, self.directory, dir_name)
38 | filename = "cssfile.css"
39 |
40 | self.assertEqual(
41 | self.dir_handler.relative_path(dir_path, filename),
42 | os.path.join(self.directory, dir_name, filename)
43 | )
44 |
45 | def testNewRelativePath(self):
46 |
47 | dir_name = "css"
48 | dir_path = os.path.join(self.root, self.directory, dir_name)
49 | filename = "cssfile.css"
50 |
51 | self.assertEqual(
52 | self.dir_handler.new_relative_path(dir_path, filename),
53 | os.path.join(self.new_directory, dir_name, filename)
54 | )
55 |
56 | def testProcess(self):
57 |
58 | self.dir_handler.process()
59 |
60 | self.assertEqual(
61 | self.dir_handler.renamer.to,
62 | os.path.join(self.root, self.new_directory)
63 | )
64 |
65 | self.assertEqual(
66 | self.dir_handler.renamer.from_,
67 | os.path.join(self.root, self.directory)
68 | )
69 |
70 |
71 | def testSuite():
72 | suite = unittest.TestSuite()
73 |
74 | suite.addTest(TestDirectoryHandler("testPath"))
75 | suite.addTest(TestDirectoryHandler("testRelativePath"))
76 | suite.addTest(TestDirectoryHandler("testNewRelativePath"))
77 | suite.addTest(TestDirectoryHandler("testProcess"))
78 |
79 | return suite
80 |
81 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import codecs
5 | import os
6 | import unittest
7 |
8 | try:
9 | from setuptools import setup, find_packages, Command
10 | except ImportError:
11 | from ez_setup import use_setuptools
12 | use_setuptools()
13 | from setuptools import setup, find_packages, Command
14 |
15 | import sphinxtogithub
16 | from sphinxtogithub.tests import (
17 | filehandler,
18 | directoryhandler,
19 | replacer,
20 | renamer,
21 | remover,
22 | layout,
23 | layoutfactory,
24 | setup as setuptest,
25 | )
26 |
27 | class RunTests(Command):
28 | description = "Run the sphinxtogithub test suite."
29 |
30 | user_options = []
31 |
32 | def initialize_options(self):
33 | pass
34 |
35 | def finalize_options(self):
36 | pass
37 |
38 | def run(self):
39 | suites = [
40 | filehandler.testSuite(),
41 | directoryhandler.testSuite(),
42 | replacer.testSuite(),
43 | renamer.testSuite(),
44 | remover.testSuite(),
45 | layout.testSuite(),
46 | layoutfactory.testSuite(),
47 | setuptest.testSuite(),
48 | ]
49 |
50 | suite = unittest.TestSuite(suites)
51 |
52 | runner = unittest.TextTestRunner()
53 |
54 | runner.run(suite)
55 |
56 |
57 | class Publish(Command):
58 | description = "Publish package to PyPi"
59 |
60 | user_options = []
61 |
62 | def initialize_options(self):
63 | pass
64 |
65 | def finalize_options(self):
66 | pass
67 |
68 | def run(self):
69 | """Publish to PyPi"""
70 |
71 | os.system("python setup.py sdist upload")
72 |
73 |
74 | long_description = codecs.open("README.rst", "r", "utf-8").read()
75 |
76 | setup(
77 | name='sphinxtogithub',
78 | version=sphinxtogithub.__version__,
79 | description=sphinxtogithub.__doc__,
80 | author=sphinxtogithub.__author__,
81 | author_email=sphinxtogithub.__contact__,
82 | url=sphinxtogithub.__homepage__,
83 | platforms=["any"],
84 | license="BSD",
85 | packages=find_packages(),
86 | scripts=["bin/sphinxtogithub"],
87 | zip_safe=False,
88 | install_requires=[],
89 | cmdclass = {"test": RunTests, "publish" : Publish},
90 | classifiers=[
91 | "Development Status :: 4 - Beta",
92 | "Operating System :: OS Independent",
93 | "Programming Language :: Python",
94 | "Environment :: Plugins",
95 | "Intended Audience :: Developers",
96 | "License :: OSI Approved :: BSD License",
97 | "Operating System :: POSIX",
98 | "Topic :: Documentation",
99 | ],
100 | long_description=long_description,
101 | )
102 |
--------------------------------------------------------------------------------
/sphinxtogithub/tests/layoutfactory.py:
--------------------------------------------------------------------------------
1 |
2 | from sphinxtogithub.tests import MockStream
3 |
4 | import sphinxtogithub
5 | import unittest
6 | import os
7 | import shutil
8 |
9 | root = "test_path"
10 | dirs = ["dir1", "dir2", "dir_", "d_ir", "_static", "_source"]
11 | files = ["file1.html", "nothtml.txt", "file2.html", "javascript.js"]
12 |
13 | def mock_is_dir(path):
14 |
15 | directories = [ os.path.join(root, dir_) for dir_ in dirs ]
16 |
17 | return path in directories
18 |
19 | def mock_list_dir(path):
20 |
21 | contents = []
22 | contents.extend(dirs)
23 | contents.extend(files)
24 | return contents
25 |
26 | def mock_walk(path):
27 |
28 | yield path, dirs, files
29 |
30 | class MockHandlerFactory(object):
31 |
32 | def create_file_handler(self, name, replacers, opener):
33 |
34 | return sphinxtogithub.FileHandler(name, replacers, opener)
35 |
36 | def create_dir_handler(self, name, root, renamer):
37 |
38 | return sphinxtogithub.DirectoryHandler(name, root, renamer)
39 |
40 |
41 | class TestLayoutFactory(unittest.TestCase):
42 |
43 | def setUp(self):
44 |
45 | verbose = True
46 | force = False
47 | stream = MockStream()
48 | dir_helper = sphinxtogithub.DirHelper(
49 | mock_is_dir,
50 | mock_list_dir,
51 | mock_walk,
52 | shutil.rmtree
53 | )
54 |
55 | file_helper = sphinxtogithub.FileSystemHelper(
56 | open,
57 | os.path.join,
58 | shutil.move,
59 | os.path.exists
60 | )
61 |
62 | operations_factory = sphinxtogithub.OperationsFactory()
63 | handler_factory = MockHandlerFactory()
64 |
65 | self.layoutfactory = sphinxtogithub.LayoutFactory(
66 | operations_factory,
67 | handler_factory,
68 | file_helper,
69 | dir_helper,
70 | verbose,
71 | stream,
72 | force
73 | )
74 |
75 | def tearDown(self):
76 |
77 | self.layoutfactory = None
78 |
79 | def testUnderscoreCheck(self):
80 |
81 | func = self.layoutfactory.is_underscore_dir
82 | self.assert_(func(root, "_static"))
83 | self.assert_(not func(root, "dir_"))
84 | self.assert_(not func(root, "d_ir"))
85 | self.assert_(not func(root, "dir1"))
86 |
87 |
88 | def testCreateLayout(self):
89 |
90 | layout = self.layoutfactory.create_layout(root)
91 |
92 | dh = layout.directory_handlers
93 | self.assertEqual(dh[0].name, "_static")
94 | self.assertEqual(dh[1].name, "_source")
95 | self.assertEqual(len(dh), 2)
96 |
97 | fh = layout.file_handlers
98 | self.assertEqual(fh[0].name, os.path.join(root,"file1.html"))
99 | self.assertEqual(fh[1].name, os.path.join(root,"file2.html"))
100 | self.assertEqual(fh[2].name, os.path.join(root,"javascript.js"))
101 | self.assertEqual(len(fh), 3)
102 |
103 |
104 |
105 |
106 | def testSuite():
107 | suite = unittest.TestSuite()
108 |
109 | suite.addTest(TestLayoutFactory("testUnderscoreCheck"))
110 | suite.addTest(TestLayoutFactory("testCreateLayout"))
111 |
112 | return suite
113 |
114 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Sphinx to GitHub
2 | ================
3 |
4 | ATTENTION!
5 | ----------
6 |
7 | This project is designed to help you get around the github-pages Jekyll
8 | behaviour of ignoring top level directories starting with an underscore.
9 |
10 | This is solved in a much neater way by creating a ``.nojekyll`` in the root
11 | of you github-pages which will disable Jekyll as described `here
12 | `__ and `here
13 | `__.
14 |
15 | This makes this project largely useless! Thank you to `acdha
16 | `__ for making me aware of this.
17 |
18 | What?
19 | -----
20 |
21 | A Python script for preparing the html output of the Sphinx documentation
22 | system for github pages.
23 |
24 | It renames any top level folders which start with an underscore and edits any
25 | references to them within the html files.
26 |
27 | Why?
28 | ----
29 |
30 | GitHub processes the incoming html with Jekyll which believes top level folders
31 | starting with an underscore are special and does not let their content be accessible
32 | to the server. This is incompatible with Sphinx which uses underscores at the
33 | start of folder names for static content.
34 |
35 | Usage
36 | -----
37 |
38 | The ``sphinxtogithub.py`` script can be run on the command line or used as a
39 | Sphinx extension.
40 |
41 | Extension
42 | ~~~~~~~~~
43 |
44 | Place the script on the ``PYTHONPATH`` and add ``sphinxtogithub`` to the
45 | extensions list in the ``conf.py`` file in your Sphinx project::
46 |
47 | extensions = [ "sphinxtogithub" ]
48 |
49 | Additionally there are three config variables you can use to control the
50 | extension. The first enables/disables the extension, the second enables verbose
51 | output and the third determines the encoding which is used to read & write
52 | files. The first two are ``True`` by default and the third is set to ``utf-8``::
53 |
54 | sphinx_to_github = True
55 | sphinx_to_github_verbose = True
56 | sphinx_to_github_encoding = "utf-8"
57 |
58 | Command Line
59 | ~~~~~~~~~~~~
60 |
61 | Run the script with the path to the ``html`` output directory as the first
62 | argument. There is a ``--verbose`` flag for basic output.
63 |
64 | Further Information
65 | -------------------
66 |
67 | Install from GitHub
68 | ~~~~~~~~~~~~~~~~~~~
69 |
70 | It should be possible to install this tool directly from github using pip::
71 |
72 | pip install -e git+git://github.com/michaeljones/sphinx-to-github.git#egg=sphinx-to-github
73 |
74 | Thanks to `winhamwr `_'s work.
75 |
76 | Requirements
77 | ~~~~~~~~~~~~
78 |
79 | The script uses ``/usr/bin/env`` and ``python``.
80 |
81 | Running Tests
82 | ~~~~~~~~~~~~~
83 |
84 | Unit tests can be run using the setuptools ``test`` target. eg::
85 |
86 | $ python setup.py test
87 |
88 | Alternatives
89 | ~~~~~~~~~~~~
90 |
91 | `dinoboff `_'s project
92 | `github-tools `_ provides similar
93 | functionality combined with a much more comprehensive set of tools for helping
94 | you to manage Python based projects on github.
95 |
96 | Credits
97 | -------
98 |
99 | Thank you to:
100 |
101 | * `mikejs `_
102 | * `certik `_
103 | * `davvid `_
104 | * `winhamwr `_
105 | * `johnpaulett `_
106 | * `boothead `_
107 | * `kennethreitz `_
108 | * `acdha `_
109 | * `garbados `_
110 |
111 | For their contributions, which are beginning to outweigh mine, to Georg Brandl
112 | for `Sphinx `_ and the github crew for the pages
113 | functionality.
114 |
115 |
116 |
--------------------------------------------------------------------------------
/sphinxtogithub/sphinxtogithub.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 |
3 | from optparse import OptionParser
4 |
5 | import os
6 | import sys
7 | import shutil
8 | import codecs
9 |
10 |
11 | class DirHelper(object):
12 |
13 | def __init__(self, is_dir, list_dir, walk, rmtree):
14 |
15 | self.is_dir = is_dir
16 | self.list_dir = list_dir
17 | self.walk = walk
18 | self.rmtree = rmtree
19 |
20 | class FileSystemHelper(object):
21 |
22 | def __init__(self, open_, path_join, move, exists):
23 |
24 | self.open_ = open_
25 | self.path_join = path_join
26 | self.move = move
27 | self.exists = exists
28 |
29 | class Replacer(object):
30 | "Encapsulates a simple text replace"
31 |
32 | def __init__(self, from_, to):
33 |
34 | self.from_ = from_
35 | self.to = to
36 |
37 | def process(self, text):
38 |
39 | return text.replace( self.from_, self.to )
40 |
41 | class FileHandler(object):
42 | "Applies a series of replacements the contents of a file inplace"
43 |
44 | def __init__(self, name, replacers, opener):
45 |
46 | self.name = name
47 | self.replacers = replacers
48 | self.opener = opener
49 |
50 | def process(self):
51 |
52 | text = self.opener(self.name, "r").read()
53 |
54 | for replacer in self.replacers:
55 | text = replacer.process( text )
56 |
57 | self.opener(self.name, "w").write(text)
58 |
59 | class Remover(object):
60 |
61 | def __init__(self, exists, remove):
62 | self.exists = exists
63 | self.remove = remove
64 |
65 | def __call__(self, name):
66 |
67 | if self.exists(name):
68 | self.remove(name)
69 |
70 | class ForceRename(object):
71 |
72 | def __init__(self, renamer, remove):
73 |
74 | self.renamer = renamer
75 | self.remove = remove
76 |
77 | def __call__(self, from_, to):
78 |
79 | self.remove(to)
80 | self.renamer(from_, to)
81 |
82 | class VerboseRename(object):
83 |
84 | def __init__(self, renamer, stream):
85 |
86 | self.renamer = renamer
87 | self.stream = stream
88 |
89 | def __call__(self, from_, to):
90 |
91 | self.stream.write(
92 | "Renaming directory '%s' -> '%s'\n"
93 | % (os.path.basename(from_), os.path.basename(to))
94 | )
95 |
96 | self.renamer(from_, to)
97 |
98 |
99 | class DirectoryHandler(object):
100 | "Encapsulates renaming a directory by removing its first character"
101 |
102 | def __init__(self, name, root, renamer):
103 |
104 | self.name = name
105 | self.new_name = name[1:]
106 | self.root = root + os.sep
107 | self.renamer = renamer
108 |
109 | def path(self):
110 |
111 | return os.path.join(self.root, self.name)
112 |
113 | def relative_path(self, directory, filename):
114 |
115 | path = directory.replace(self.root, "", 1)
116 | return os.path.join(path, filename)
117 |
118 | def new_relative_path(self, directory, filename):
119 |
120 | path = self.relative_path(directory, filename)
121 | return path.replace(self.name, self.new_name, 1)
122 |
123 | def process(self):
124 |
125 | from_ = os.path.join(self.root, self.name)
126 | to = os.path.join(self.root, self.new_name)
127 | self.renamer(from_, to)
128 |
129 |
130 | class HandlerFactory(object):
131 |
132 | def create_file_handler(self, name, replacers, opener):
133 |
134 | return FileHandler(name, replacers, opener)
135 |
136 | def create_dir_handler(self, name, root, renamer):
137 |
138 | return DirectoryHandler(name, root, renamer)
139 |
140 |
141 | class OperationsFactory(object):
142 |
143 | def create_force_rename(self, renamer, remover):
144 |
145 | return ForceRename(renamer, remover)
146 |
147 | def create_verbose_rename(self, renamer, stream):
148 |
149 | return VerboseRename(renamer, stream)
150 |
151 | def create_replacer(self, from_, to):
152 |
153 | return Replacer(from_, to)
154 |
155 | def create_remover(self, exists, remove):
156 |
157 | return Remover(exists, remove)
158 |
159 |
160 | class Layout(object):
161 | """
162 | Applies a set of operations which result in the layout
163 | of a directory changing
164 | """
165 |
166 | def __init__(self, directory_handlers, file_handlers):
167 |
168 | self.directory_handlers = directory_handlers
169 | self.file_handlers = file_handlers
170 |
171 | def process(self):
172 |
173 | for handler in self.file_handlers:
174 | handler.process()
175 |
176 | for handler in self.directory_handlers:
177 | handler.process()
178 |
179 |
180 | class NullLayout(object):
181 | """
182 | Layout class that does nothing when asked to process
183 | """
184 | def process(self):
185 | pass
186 |
187 | class LayoutFactory(object):
188 | "Creates a layout object"
189 |
190 | def __init__(self, operations_factory, handler_factory, file_helper, dir_helper, verbose, stream, force):
191 |
192 | self.operations_factory = operations_factory
193 | self.handler_factory = handler_factory
194 |
195 | self.file_helper = file_helper
196 | self.dir_helper = dir_helper
197 |
198 | self.verbose = verbose
199 | self.output_stream = stream
200 | self.force = force
201 |
202 | def create_layout(self, path):
203 |
204 | contents = self.dir_helper.list_dir(path)
205 |
206 | renamer = self.file_helper.move
207 |
208 | if self.force:
209 | remove = self.operations_factory.create_remover(self.file_helper.exists, self.dir_helper.rmtree)
210 | renamer = self.operations_factory.create_force_rename(renamer, remove)
211 |
212 | if self.verbose:
213 | renamer = self.operations_factory.create_verbose_rename(renamer, self.output_stream)
214 |
215 | # Build list of directories to process
216 | directories = [d for d in contents if self.is_underscore_dir(path, d)]
217 | underscore_directories = [
218 | self.handler_factory.create_dir_handler(d, path, renamer)
219 | for d in directories
220 | ]
221 |
222 | if not underscore_directories:
223 | if self.verbose:
224 | self.output_stream.write(
225 | "No top level directories starting with an underscore "
226 | "were found in '%s'\n" % path
227 | )
228 | return NullLayout()
229 |
230 | # Build list of files that are in those directories
231 | replacers = []
232 | for handler in underscore_directories:
233 | for directory, dirs, files in self.dir_helper.walk(handler.path()):
234 | for f in files:
235 | replacers.append(
236 | self.operations_factory.create_replacer(
237 | handler.relative_path(directory, f),
238 | handler.new_relative_path(directory, f)
239 | )
240 | )
241 |
242 | # Build list of handlers to process all files
243 | filelist = []
244 | for root, dirs, files in self.dir_helper.walk(path):
245 | for f in files:
246 | if f.endswith(".html"):
247 | filelist.append(
248 | self.handler_factory.create_file_handler(
249 | self.file_helper.path_join(root, f),
250 | replacers,
251 | self.file_helper.open_)
252 | )
253 | if f.endswith(".js"):
254 | filelist.append(
255 | self.handler_factory.create_file_handler(
256 | self.file_helper.path_join(root, f),
257 | [self.operations_factory.create_replacer("'_sources/'", "'sources/'")],
258 | self.file_helper.open_
259 | )
260 | )
261 |
262 | return Layout(underscore_directories, filelist)
263 |
264 | def is_underscore_dir(self, path, directory):
265 |
266 | return (self.dir_helper.is_dir(self.file_helper.path_join(path, directory))
267 | and directory.startswith("_"))
268 |
269 |
270 |
271 | def sphinx_extension(app, exception):
272 | "Wrapped up as a Sphinx Extension"
273 |
274 | if not app.builder.name in ("html", "dirhtml"):
275 | return
276 |
277 | if not app.config.sphinx_to_github:
278 | if app.config.sphinx_to_github_verbose:
279 | print "Sphinx-to-github: Disabled, doing nothing."
280 | return
281 |
282 | if exception:
283 | if app.config.sphinx_to_github_verbose:
284 | print "Sphinx-to-github: Exception raised in main build, doing nothing."
285 | return
286 |
287 | dir_helper = DirHelper(
288 | os.path.isdir,
289 | os.listdir,
290 | os.walk,
291 | shutil.rmtree
292 | )
293 |
294 | file_helper = FileSystemHelper(
295 | lambda f, mode: codecs.open(f, mode, app.config.sphinx_to_github_encoding),
296 | os.path.join,
297 | shutil.move,
298 | os.path.exists
299 | )
300 |
301 | operations_factory = OperationsFactory()
302 | handler_factory = HandlerFactory()
303 |
304 | layout_factory = LayoutFactory(
305 | operations_factory,
306 | handler_factory,
307 | file_helper,
308 | dir_helper,
309 | app.config.sphinx_to_github_verbose,
310 | sys.stdout,
311 | force=True
312 | )
313 |
314 | layout = layout_factory.create_layout(app.outdir)
315 | layout.process()
316 |
317 |
318 | def setup(app):
319 | "Setup function for Sphinx Extension"
320 |
321 | app.add_config_value("sphinx_to_github", True, '')
322 | app.add_config_value("sphinx_to_github_verbose", True, '')
323 | app.add_config_value("sphinx_to_github_encoding", 'utf-8', '')
324 |
325 | app.connect("build-finished", sphinx_extension)
326 |
327 |
328 | def main(args):
329 |
330 | usage = "usage: %prog [options] "
331 | parser = OptionParser(usage=usage)
332 | parser.add_option("-v","--verbose", action="store_true",
333 | dest="verbose", default=False, help="Provides verbose output")
334 | parser.add_option("-e","--encoding", action="store",
335 | dest="encoding", default="utf-8", help="Encoding for reading and writing files")
336 | opts, args = parser.parse_args(args)
337 |
338 | try:
339 | path = args[0]
340 | except IndexError:
341 | sys.stderr.write(
342 | "Error - Expecting path to html directory:"
343 | "sphinx-to-github \n"
344 | )
345 | return
346 |
347 | dir_helper = DirHelper(
348 | os.path.isdir,
349 | os.listdir,
350 | os.walk,
351 | shutil.rmtree
352 | )
353 |
354 | file_helper = FileSystemHelper(
355 | lambda f, mode: codecs.open(f, mode, opts.encoding),
356 | os.path.join,
357 | shutil.move,
358 | os.path.exists
359 | )
360 |
361 | operations_factory = OperationsFactory()
362 | handler_factory = HandlerFactory()
363 |
364 | layout_factory = LayoutFactory(
365 | operations_factory,
366 | handler_factory,
367 | file_helper,
368 | dir_helper,
369 | opts.verbose,
370 | sys.stdout,
371 | force=False
372 | )
373 |
374 | layout = layout_factory.create_layout(path)
375 | layout.process()
376 |
377 |
378 |
379 | if __name__ == "__main__":
380 | main(sys.argv[1:])
381 |
382 |
383 |
384 |
--------------------------------------------------------------------------------