Skip to content

Commit ff05288

Browse files
committed
Import BuildDoc from sphinx (fixes #987)
1 parent d540613 commit ff05288

File tree

2 files changed

+229
-3
lines changed

2 files changed

+229
-3
lines changed

LICENSE

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,31 @@ products or services of Licensee, or any third party.
7272
8. By copying, installing or otherwise using Python, Licensee
7373
agrees to be bound by the terms and conditions of this License
7474
Agreement.
75+
76+
77+
BuildDoc in setup.py is licensed under the BSD-2 license:
78+
79+
Copyright 2007-2021 Sebastian Wiesner
80+
81+
Redistribution and use in source and binary forms, with or without
82+
modification, are permitted provided that the following conditions are
83+
met:
84+
85+
* Redistributions of source code must retain the above copyright
86+
notice, this list of conditions and the following disclaimer.
87+
88+
* Redistributions in binary form must reproduce the above copyright
89+
notice, this list of conditions and the following disclaimer in the
90+
documentation and/or other materials provided with the distribution.
91+
92+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
93+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
94+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
95+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
96+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
97+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
98+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
99+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
100+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
101+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
102+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

setup.py

Lines changed: 201 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import re
66
import subprocess
77

8-
from setuptools import setup
8+
from setuptools import setup, Command
99
from setuptools.command.build import build as _orig_build
1010

1111
try:
@@ -17,14 +17,212 @@
1717

1818
try:
1919
import sphinx
20-
from sphinx.setup_command import BuildDoc
2120

2221
# Sphinx 1.5 and newer support Python 3.6
23-
using_sphinx = sphinx.__version__ >= "1.5" and sphinx.__version__ < "7.0"
22+
using_sphinx = sphinx.__version__ >= "1.5"
2423
except ImportError:
2524
using_sphinx = False
2625

2726

27+
if using_sphinx:
28+
import sys
29+
from io import StringIO
30+
31+
from setuptools.errors import ExecError
32+
from sphinx.application import Sphinx
33+
from sphinx.cmd.build import handle_exception
34+
from sphinx.util.console import color_terminal, nocolor
35+
from sphinx.util.docutils import docutils_namespace, patch_docutils
36+
from sphinx.util.osutil import abspath
37+
38+
class BuildDoc(Command):
39+
"""
40+
Distutils command to build Sphinx documentation.
41+
The Sphinx build can then be triggered from distutils, and some Sphinx
42+
options can be set in ``setup.py`` or ``setup.cfg`` instead of Sphinx's
43+
own configuration file.
44+
For instance, from `setup.py`::
45+
# this is only necessary when not using setuptools/distribute
46+
from sphinx.setup_command import BuildDoc
47+
cmdclass = {'build_sphinx': BuildDoc}
48+
name = 'My project'
49+
version = '1.2'
50+
release = '1.2.0'
51+
setup(
52+
name=name,
53+
author='Bernard Montgomery',
54+
version=release,
55+
cmdclass=cmdclass,
56+
# these are optional and override conf.py settings
57+
command_options={
58+
'build_sphinx': {
59+
'project': ('setup.py', name),
60+
'version': ('setup.py', version),
61+
'release': ('setup.py', release)}},
62+
)
63+
Or add this section in ``setup.cfg``::
64+
[build_sphinx]
65+
project = 'My project'
66+
version = 1.2
67+
release = 1.2.0
68+
"""
69+
70+
description = "Build Sphinx documentation"
71+
user_options = [
72+
("fresh-env", "E", "discard saved environment"),
73+
("all-files", "a", "build all files"),
74+
("source-dir=", "s", "Source directory"),
75+
("build-dir=", None, "Build directory"),
76+
("config-dir=", "c", "Location of the configuration directory"),
77+
(
78+
"builder=",
79+
"b",
80+
"The builder (or builders) to use. Can be a comma- "
81+
'or space-separated list. Defaults to "html"',
82+
),
83+
("warning-is-error", "W", "Turn warning into errors"),
84+
("project=", None, "The documented project's name"),
85+
("version=", None, "The short X.Y version"),
86+
(
87+
"release=",
88+
None,
89+
"The full version, including alpha/beta/rc tags",
90+
),
91+
(
92+
"today=",
93+
None,
94+
"How to format the current date, used as the "
95+
"replacement for |today|",
96+
),
97+
("link-index", "i", "Link index.html to the master doc"),
98+
("copyright", None, "The copyright string"),
99+
("pdb", None, "Start pdb on exception"),
100+
("verbosity", "v", "increase verbosity (can be repeated)"),
101+
(
102+
"nitpicky",
103+
"n",
104+
"nit-picky mode, warn about all missing references",
105+
),
106+
("keep-going", None, "With -W, keep going when getting warnings"),
107+
]
108+
boolean_options = [
109+
"fresh-env",
110+
"all-files",
111+
"warning-is-error",
112+
"link-index",
113+
"nitpicky",
114+
]
115+
116+
def initialize_options(self) -> None:
117+
self.fresh_env = self.all_files = False
118+
self.pdb = False
119+
self.source_dir: str = None
120+
self.build_dir: str = None
121+
self.builder = "html"
122+
self.warning_is_error = False
123+
self.project = ""
124+
self.version = ""
125+
self.release = ""
126+
self.today = ""
127+
self.config_dir: str = None
128+
self.link_index = False
129+
self.copyright = ""
130+
# Link verbosity to distutils' (which uses 1 by default).
131+
self.verbosity = self.distribution.verbose - 1 # type: ignore
132+
self.traceback = False
133+
self.nitpicky = False
134+
self.keep_going = False
135+
136+
def _guess_source_dir(self) -> str:
137+
for guess in ("doc", "docs"):
138+
if not os.path.isdir(guess):
139+
continue
140+
for root, dirnames, filenames in os.walk(guess):
141+
if "conf.py" in filenames:
142+
return root
143+
return os.curdir
144+
145+
def finalize_options(self) -> None:
146+
self.ensure_string_list("builder")
147+
148+
if self.source_dir is None:
149+
self.source_dir = self._guess_source_dir()
150+
self.announce("Using source directory %s" % self.source_dir)
151+
152+
self.ensure_dirname("source_dir")
153+
154+
if self.config_dir is None:
155+
self.config_dir = self.source_dir
156+
157+
if self.build_dir is None:
158+
build = self.get_finalized_command("build")
159+
self.build_dir = os.path.join(abspath(build.build_base), "sphinx") # type: ignore
160+
161+
self.doctree_dir = os.path.join(self.build_dir, "doctrees")
162+
163+
self.builder_target_dirs = [
164+
(builder, os.path.join(self.build_dir, builder))
165+
for builder in self.builder
166+
]
167+
168+
def run(self) -> None:
169+
if not color_terminal():
170+
nocolor()
171+
if not self.verbose: # type: ignore
172+
status_stream = StringIO()
173+
else:
174+
status_stream = sys.stdout # type: ignore
175+
confoverrides = {}
176+
if self.project:
177+
confoverrides["project"] = self.project
178+
if self.version:
179+
confoverrides["version"] = self.version
180+
if self.release:
181+
confoverrides["release"] = self.release
182+
if self.today:
183+
confoverrides["today"] = self.today
184+
if self.copyright:
185+
confoverrides["copyright"] = self.copyright
186+
if self.nitpicky:
187+
confoverrides["nitpicky"] = self.nitpicky
188+
189+
for builder, builder_target_dir in self.builder_target_dirs:
190+
app = None
191+
192+
try:
193+
confdir = self.config_dir or self.source_dir
194+
with patch_docutils(confdir), docutils_namespace():
195+
app = Sphinx(
196+
self.source_dir,
197+
self.config_dir,
198+
builder_target_dir,
199+
self.doctree_dir,
200+
builder,
201+
confoverrides,
202+
status_stream,
203+
freshenv=self.fresh_env,
204+
warningiserror=self.warning_is_error,
205+
verbosity=self.verbosity,
206+
keep_going=self.keep_going,
207+
)
208+
app.build(force_all=self.all_files)
209+
if app.statuscode:
210+
raise ExecError(
211+
"caused by %s builder." % app.builder.name
212+
)
213+
except Exception as exc:
214+
handle_exception(app, self, exc, sys.stderr)
215+
if not self.pdb:
216+
raise SystemExit(1) from exc
217+
218+
if not self.link_index:
219+
continue
220+
221+
src = app.config.root_doc + app.builder.out_suffix # type: ignore
222+
dst = app.builder.get_outfilename("index") # type: ignore
223+
os.symlink(src, dst)
224+
225+
28226
# version handling
29227

30228

0 commit comments

Comments
 (0)