Skip to content

Commit

Permalink
sphinx-build: Guess sourcedir if none provided
Browse files Browse the repository at this point in the history
This is part two of two of the "make 'sphinx-build' easier to call"
effort. In this part, we attempt to guess where the source directory is
if the user does not provide one. This is possible because many users
use one of three directory names - doc, docs, or Documentation - for
their documentation. We check these, along with a possible 'source'
subdirectory, which is based on Sphinx's own defaults, for a 'conf.py'
file. Is one is found, we assume this is the documentation source. This
means a user no longer needs to pass any argument to 'sphinx-build' is
they do not wish to.

This is, of course, best-effort, and users can continue to provide these
options manually if they so wish.

Signed-off-by: Stephen Finucane <stephen@that.guru>
  • Loading branch information
stephenfin committed Dec 29, 2017
1 parent baeb258 commit 97c2550
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 15 deletions.
6 changes: 4 additions & 2 deletions doc/man/sphinx-build.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ sphinx-build
Synopsis
--------

**sphinx-build** [*options*] <*sourcedir*> [<*outputdir*> [*filenames* ...]]
**sphinx-build** [*options*] [<*sourcedir*> [<*outputdir*> [*filenames* ...]]]

Description
-----------

:program:`sphinx-build` generates documentation from the files in
``<sourcedir>`` and places it in the ``<outputdir>``. If no ``<outputdir>`` is
provided, Sphinx will attempt to use the ``output_dir`` configuration option to
configure this.
configure this. If no ``<sourcedir>`` is provided, Sphinx will attempt to find
documentation in a list of commonly used documentation source folders relative
to where the application is run from.

:program:`sphinx-build` looks for ``<sourcedir>/conf.py`` for the configuration
settings. :manpage:`sphinx-quickstart(1)` may be used to generate template
Expand Down
54 changes: 42 additions & 12 deletions sphinx/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,21 @@

if False:
# For type annotation
from typing import Any, IO, List, Union # NOQA
from typing import Any, IO, List, Optional, Union # NOQA

DOCS_DIRS = ('docs', 'doc', 'Documentation', '')


def _find_source_dir():
# type: () -> Optional[unicode]
"""Check the usual suspects for a config file and return if found."""
src_dirs = list(DOCS_DIRS) + [path.join(x, 'source') for x in DOCS_DIRS]
for src_dir in src_dirs:
conf_file = path.join(src_dir, 'conf.py')
# we need to check this early because a doc dir could be nested
if path.exists(conf_file):
return src_dir
return None


def handle_exception(app, args, exception, stderr=sys.stderr):
Expand Down Expand Up @@ -86,8 +100,8 @@ def handle_exception(app, args, exception, stderr=sys.stderr):
def get_parser():
# type: () -> argparse.ArgumentParser
parser = argparse.ArgumentParser(
usage='usage: %(prog)s [OPTIONS] SOURCEDIR [OUTPUTDIR '
'[FILENAMES...]]',
usage='usage: %(prog)s [OPTIONS] [SOURCEDIR [OUTPUTDIR '
'[FILENAMES...]]]',
epilog='For more information, visit <http://sphinx-doc.org/>.',
description="""
Generate documentation from source files.
Expand All @@ -109,7 +123,7 @@ def get_parser():
parser.add_argument('--version', action='version', dest='show_version',
version='%%(prog)s %s' % __display_version__)

parser.add_argument('sourcedir',
parser.add_argument('sourcedir', nargs='?',
help='path to documentation source files')
parser.add_argument('outputdir', nargs='?',
help='path to output directory')
Expand Down Expand Up @@ -185,21 +199,36 @@ def main(argv=sys.argv[1:]): # type: ignore
parser = get_parser()
args = parser.parse_args(argv)

# get paths (first and second positional argument)
# if we haven't specified an outputdir then we need to extract one from the
# config file. Clearly we can't do this is no config file is available
if not args.outputdir and args.noconfig:
parser.error('must specify sourcedir with -C option')

# get paths from args or by discovery
if args.sourcedir:
srcdir = args.sourcedir
confdir = args.confdir or srcdir
else:
srcdir = confdir = _find_source_dir()
if not srcdir:
parser.error('no sourcedir provided and none discovered')

try:
srcdir = abspath(args.sourcedir)
confdir = abspath(args.confdir or srcdir)
srcdir = abspath(srcdir)
confdir = abspath(confdir)
except UnicodeError:
parser.error('multibyte filename not supported on this filesystem '
'encoding (%r).' % fs_encoding)

if args.sourcedir:
if args.noconfig:
confdir = None

if not path.isdir(srcdir):
parser.error('cannot find source directory (%s)' % srcdir)
if not args.noconfig and not path.isfile(path.join(confdir, 'conf.py')):
if confdir and not path.isfile(path.join(confdir, 'conf.py')):
parser.error("config directory doesn't contain a conf.py file "
"(%s)" % confdir)
except UnicodeError:
parser.error('multibyte filename not supported on this filesystem '
'encoding (%r)' % fs_encoding)

# if outputdir is not provided, we will attempt to extract it from
# configuration later
Expand All @@ -211,7 +240,8 @@ def main(argv=sys.argv[1:]): # type: ignore
'encoding (%r).' % fs_encoding)

if srcdir == outdir:
parser.error('source directory and destination directory are same')
parser.error('source directory and destination directory are '
'same')
else:
outdir = None

Expand Down
35 changes: 34 additions & 1 deletion tests/test_cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,23 @@
:license: BSD, see LICENSE for details.
"""

import os.path

import mock

from sphinx.cmdline import main
from sphinx.cmdline import main, _find_source_dir


@mock.patch('os.path.exists')
def test_find_source_dir(mock_exists):
"""Test basic behavior of source dir."""
src_dir = os.path.join('doc', 'source')

def path_exists(path):
return path == os.path.join(src_dir, 'conf.py')

mock_exists.side_effect = path_exists
assert _find_source_dir() == src_dir


def fake_abspath(path):
Expand Down Expand Up @@ -63,3 +77,22 @@ def test_posargs_no_outputdir(mock_sphinx, mock_abspath, mock_isdir,
args[0], args[0], None, None, mock.ANY, mock.ANY, mock.ANY, mock.ANY,
mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY)
mock_sphinx.return_value.build.assert_called_once_with(False, [])


@mock.patch('sphinx.cmdline._find_source_dir')
@mock.patch('os.path.isfile', return_value=True)
@mock.patch('os.path.isdir', return_value=True)
@mock.patch('sphinx.cmdline.abspath', side_effect=fake_abspath)
@mock.patch('sphinx.cmdline.Sphinx')
def test_posargs_none(mock_sphinx, mock_abspath, mock_isdir,
mock_isfile, mock_find_src):
"""Validate behavior with no posargs."""
args = []
main(args)

srcdir = mock_find_src.return_value

mock_sphinx.assert_called_once_with(
srcdir, srcdir, None, None, mock.ANY, mock.ANY, mock.ANY, mock.ANY,
mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY)
mock_sphinx.return_value.build.assert_called_once_with(False, [])

0 comments on commit 97c2550

Please sign in to comment.