diff --git a/CHANGES.rst b/CHANGES.rst index 062d59a8..d7bcc6bd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,9 @@ astropy-helpers Changelog logo and linkout image, falling back to PNGs for browsers that support it. [#151] +- Various fixes enabling the astropy-helpers Sphinx build command and + Sphinx extensions to work with Sphinx 1.3. [#148] + 1.0.1 (2015-03-04) ------------------ diff --git a/astropy_helpers/commands/build_sphinx.py b/astropy_helpers/commands/build_sphinx.py index c52115af..4517e92c 100644 --- a/astropy_helpers/commands/build_sphinx.py +++ b/astropy_helpers/commands/build_sphinx.py @@ -10,8 +10,11 @@ from distutils import log from distutils.cmd import DistutilsOptionError +import sphinx from sphinx.setup_command import BuildDoc as SphinxBuildDoc +from ..utils import minversion + PY3 = sys.version_info[0] >= 3 @@ -146,6 +149,12 @@ def run(self): subproccode[i] = repr(val) subproccode = ''.join(subproccode) + # This is a quick gross hack, but it ensures that the code grabbed from + # SphinxBuildDoc.run will work in Python 2 if it uses the print + # function + if minversion(sphinx, '1.3'): + subproccode = 'from __future__ import print_function' + subproccode + if self.no_intersphinx: # the confoverrides variable in sphinx.setup_command.BuildDoc can # be used to override the conf.py ... but this could well break diff --git a/astropy_helpers/sphinx/ext/autodoc_enhancements.py b/astropy_helpers/sphinx/ext/autodoc_enhancements.py index ee638141..9a1b64f1 100644 --- a/astropy_helpers/sphinx/ext/autodoc_enhancements.py +++ b/astropy_helpers/sphinx/ext/autodoc_enhancements.py @@ -43,13 +43,16 @@ def type_object_attrgetter(obj, attr, *defargs): of autodoc. """ - if attr in obj.__dict__ and isinstance(obj.__dict__[attr], property): - # Note, this should only be used for properties--for any other type of - # descriptor (classmethod, for example) this can mess up existing - # expectcations of what getattr(cls, ...) returns - return obj.__dict__[attr] - else: - return getattr(obj, attr, *defargs) + for base in obj.__mro__: + if attr in base.__dict__: + if isinstance(base.__dict__[attr], property): + # Note, this should only be used for properties--for any other + # type of descriptor (classmethod, for example) this can mess + # up existing expectations of what getattr(cls, ...) returns + return base.__dict__[attr] + break + + return getattr(obj, attr, *defargs) def setup(app): diff --git a/astropy_helpers/sphinx/ext/automodapi.py b/astropy_helpers/sphinx/ext/automodapi.py index db7c4c05..19b628d3 100644 --- a/astropy_helpers/sphinx/ext/automodapi.py +++ b/astropy_helpers/sphinx/ext/automodapi.py @@ -85,6 +85,11 @@ from .utils import find_mod_objs +if sys.version_info[0] == 3: + text_type = str +else: + text_type = unicode + automod_templ_modheader = """ {modname} {pkgormod} @@ -296,7 +301,7 @@ def automodapi_replace(sourcestr, app, dotoctree=True, docname=None, if app.config.automodapi_writereprocessed: # sometimes they are unicode, sometimes not, depending on how # sphinx has processed things - if isinstance(newsourcestr, unicode): + if isinstance(newsourcestr, text_type): ustr = newsourcestr else: ustr = newsourcestr.decode(app.config.source_encoding) @@ -304,10 +309,16 @@ def automodapi_replace(sourcestr, app, dotoctree=True, docname=None, if docname is None: with open(os.path.join(app.srcdir, 'unknown.automodapi'), 'a') as f: f.write('\n**NEW DOC**\n\n') - f.write(ustr.encode('utf8')) + f.write(ustr) else: - with open(os.path.join(app.srcdir, docname + '.automodapi'), 'w') as f: - f.write(ustr.encode('utf8')) + env = app.builder.env + # Determine the filename associated with this doc (specifically + # the extension) + filename = docname + os.path.splitext(env.doc2path(docname))[1] + filename += '.automodapi' + + with open(os.path.join(app.srcdir, filename), 'w') as f: + f.write(ustr) return newsourcestr else: @@ -330,8 +341,11 @@ def _mod_info(modname, toskip=[], onlylocals=True): break # find_mod_objs has already imported modname + # TODO: There is probably a cleaner way to do this, though this is pretty + # reliable for all Python versions for most cases that we care about. pkg = sys.modules[modname] - ispkg = '__init__.' in os.path.split(pkg.__name__)[1] + ispkg = (hasattr(pkg, '__file__') and isinstance(pkg.__file__, str) and + os.path.split(pkg.__file__)[1].startswith('__init__.py')) return ispkg, hascls, hasfunc diff --git a/astropy_helpers/sphinx/ext/automodsumm.py b/astropy_helpers/sphinx/ext/automodsumm.py index 9a436807..4d0e5969 100644 --- a/astropy_helpers/sphinx/ext/automodsumm.py +++ b/astropy_helpers/sphinx/ext/automodsumm.py @@ -172,18 +172,23 @@ def run(self): self.content = cont - #for some reason, even though ``currentmodule`` is substituted in, sphinx - #doesn't necessarily recognize this fact. So we just force it - #internally, and that seems to fix things + # for some reason, even though ``currentmodule`` is substituted in, + # sphinx doesn't necessarily recognize this fact. So we just force + # it internally, and that seems to fix things env.temp_data['py:module'] = modname - #can't use super because Sphinx/docutils has trouble - #return super(Autosummary,self).run() + # can't use super because Sphinx/docutils has trouble return + # super(Autosummary,self).run() nodelist.extend(Autosummary.run(self)) + return self.warnings + nodelist finally: # has_content = False for the Automodsumm self.content = [] + def get_items(self, names): + self.genopt['imported-members'] = True + return Autosummary.get_items(self, names) + #<-------------------automod-diagram stuff------------------------------------> class Automoddiagram(InheritanceDiagram): @@ -220,10 +225,12 @@ def run(self): #<---------------------automodsumm generation stuff---------------------------> def process_automodsumm_generation(app): env = app.builder.env - ext = app.config.source_suffix - filestosearch = [x + ext for x in env.found_docs - if os.path.isfile(env.doc2path(x))]\ + filestosearch = [] + for docname in env.found_docs: + filename = env.doc2path(docname) + if os.path.isfile(filename): + filestosearch.append(docname + os.path.splitext(filename)[1]) liness = [] for sfn in filestosearch: @@ -238,10 +245,11 @@ def process_automodsumm_generation(app): f.write('\n') for sfn, lines in zip(filestosearch, liness): + suffix = os.path.splitext(sfn)[1] if len(lines) > 0: generate_automodsumm_docs(lines, sfn, builder=app.builder, warn=app.warn, info=app.info, - suffix=app.config.source_suffix, + suffix=suffix, base_path=app.srcdir) #_automodsummrex = re.compile(r'^(\s*)\.\. automodsumm::\s*([A-Za-z0-9_.]+)\s*' @@ -281,6 +289,7 @@ def automodsumm_to_autosummary_lines(fn, app): """ + fullfn = os.path.join(app.builder.env.srcdir, fn) with open(fullfn) as fr: @@ -288,7 +297,8 @@ def automodsumm_to_autosummary_lines(fn, app): from astropy_helpers.sphinx.ext.automodapi import automodapi_replace # Must do the automodapi on the source to get the automodsumm # that might be in there - filestr = automodapi_replace(fr.read(), app, True, fn, False) + docname = os.path.splitext(fn)[0] + filestr = automodapi_replace(fr.read(), app, True, docname, False) else: filestr = fr.read() @@ -353,6 +363,9 @@ def automodsumm_to_autosummary_lines(fn, app): continue newlines.append(allindent + nm) + # add one newline at the end of the autosummary block + newlines.append('') + return newlines diff --git a/astropy_helpers/sphinx/ext/tests/test_automodsumm.py b/astropy_helpers/sphinx/ext/tests/test_automodsumm.py index cd8afa33..aec7039b 100644 --- a/astropy_helpers/sphinx/ext/tests/test_automodsumm.py +++ b/astropy_helpers/sphinx/ext/tests/test_automodsumm.py @@ -66,7 +66,8 @@ def warn(self, msg, loc): automodsumm_to_autosummary_lines generate_automodsumm_docs process_automodsumm_generation - setup""" + setup +""" def test_ams_to_asmry(tmpdir): @@ -97,7 +98,8 @@ def test_ams_to_asmry(tmpdir): .. autosummary:: :p: - pilot""" + pilot +""" def test_ams_cython(tmpdir, cython_testpackage): diff --git a/astropy_helpers/sphinx/ext/viewcode.py b/astropy_helpers/sphinx/ext/viewcode.py index dc428a10..d9fdc612 100644 --- a/astropy_helpers/sphinx/ext/viewcode.py +++ b/astropy_helpers/sphinx/ext/viewcode.py @@ -16,6 +16,7 @@ from sphinx import addnodes from sphinx.locale import _ from sphinx.pycode import ModuleAnalyzer +from sphinx.util.inspect import safe_getattr from sphinx.util.nodes import make_refnode import sys @@ -51,12 +52,12 @@ def get_full_modname(modname, attribute): value = module for attr in attribute.split('.'): if attr: - value = getattr(value, attr) + value = safe_getattr(value, attr) except AttributeError: app.warn('Didn\'t find %s in %s' % (attribute, module.__name__)) return None else: - return getattr(value, '__module__', None) + return safe_getattr(value, '__module__', None) def has_tag(modname, fullname, docname, refname): diff --git a/astropy_helpers/utils.py b/astropy_helpers/utils.py index bf5bc2b8..9c053414 100644 --- a/astropy_helpers/utils.py +++ b/astropy_helpers/utils.py @@ -592,3 +592,70 @@ def delete(self): delattr(self, private_name) return property(get, set, delete) + + +def minversion(module, version, inclusive=True, version_path='__version__'): + """ + Returns `True` if the specified Python module satisfies a minimum version + requirement, and `False` if not. + + By default this uses `pkg_resources.parse_version` to do the version + comparison if available. Otherwise it falls back on + `distutils.version.LooseVersion`. + + Parameters + ---------- + + module : module or `str` + An imported module of which to check the version, or the name of + that module (in which case an import of that module is attempted-- + if this fails `False` is returned). + + version : `str` + The version as a string that this module must have at a minimum (e.g. + ``'0.12'``). + + inclusive : `bool` + The specified version meets the requirement inclusively (i.e. ``>=``) + as opposed to strictly greater than (default: `True`). + + version_path : `str` + A dotted attribute path to follow in the module for the version. + Defaults to just ``'__version__'``, which should work for most Python + modules. + + Examples + -------- + + >>> import astropy + >>> minversion(astropy, '0.4.4') + True + """ + + if isinstance(module, types.ModuleType): + module_name = module.__name__ + elif isinstance(module, six.string_types): + module_name = module + try: + module = resolve_name(module_name) + except ImportError: + return False + else: + raise ValueError('module argument must be an actual imported ' + 'module, or the import name of the module; ' + 'got {0!r}'.format(module)) + + if '.' not in version_path: + have_version = getattr(module, version_path) + else: + have_version = resolve_name('.'.join([module.__name__, version_path])) + + try: + from pkg_resources import parse_version + except ImportError: + from distutils.version import LooseVersion as parse_version + + if inclusive: + return parse_version(have_version) >= parse_version(version) + else: + return parse_version(have_version) > parse_version(version)