diff --git a/.gitignore b/.gitignore index 8f9b3ad..78caebd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.so *.pyd __pycache__ +.vimsession # Ignore .c files by default to avoid including generated code. If you want to # add a non-generated .c extension, use `git add -f filename.c`. diff --git a/README.rst b/README.rst index 48c0f11..1be3b0d 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,15 @@ available as a standalone package since it can be used for any other package. The documentation can be found on `ReadTheDocs `_. +Proplot modifications +--------------------- +I forked this repo for use with my `proplot `__ project and added the following features: + +* Skip over class methods that are public, but do *not* have their own ``__doc__`` attributes. This prevents inheriting and displaying documentation from external projects, namely matplotlib. +* Include ``__getitem__``, ``__getattr__``, ``__setitem__``, and ``__setattr__`` in the list of builtin methods that are *not* ignored by the documentation generator. I use these to document some dictionary/list subclasses. +* Give class methods and attributes their own stub pages, instead of putting all class methods and attributes on a single page. This also required adding the new files to ``env.found_docs`` and reordering the event hooks in ``automodsumm.py`` so that ``sphinx_automodapi`` is called before ``autosummary``. This way ``autosummary`` sees the new class pages and can build the appropriate stubs. + + Running tests ------------- diff --git a/hooks/README.md b/hooks/README.md new file mode 100644 index 0000000..e6bf4b3 --- /dev/null +++ b/hooks/README.md @@ -0,0 +1,2 @@ +# Git hooks +When you clone this repo, copy the `post-commit` script to `.git/hooks`. This hook tags the commit with a version number if the version string in `__init__.py` was changed. diff --git a/hooks/post-commit b/hooks/post-commit new file mode 100755 index 0000000..b794c25 --- /dev/null +++ b/hooks/post-commit @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +#-----------------------------------------------------------------------------# +# Post commit hook that looks for __version__ variable in __init__.py file, +# compares it to the current tag, and updates the commit if the __version__ was +# also updated. Copy to .git/hooks/post-commit to add this to any project. +# See also: https://stackoverflow.com/a/27332476/4970632 +# See also: https://stackoverflow.com/a/46972376/4970632 +# This is currently very lightweight and assumes workflow where users +# manually increment tag number, which is probably appropriate +#-----------------------------------------------------------------------------# +# Helper +raise() { + echo "Error: $@" + exit 1 +} + +# Bail if we are in middle of rebase +base=$(git rev-parse --show-toplevel) +[ $? -ne 0 ] && raise "Not in git repository." +[ -d $base/.git/rebase-merge ] && exit 0 + +# Get head dir +init=($(git ls-files | grep __init__.py | grep -v 'tests')) +[ ${#init[@]} -eq 0 ] && raise "__init__.py not found." +[ ${#init[@]} -gt 1 ] && raise "Multiple candidates for __init__.py: ${init[@]}" + +# Get version string +version=$(cat $init | grep -E '^version|^__version') +[ -z "$version" ] && raise "$init version string not found." +[ $(echo "$version" | wc -l) -gt 1 ] && raise "Ambiguous version string in $init." +version=$(echo "$version" | awk -F"['\"]" '{print $2}') # first string on line + +# Prompt user action +# NOTE: Currently git suppresses interactivity but that's fine, just means +# default action of adding tag is taken +tag=$(git describe --tags $(git rev-list --tags --max-count=1)) +if [ $? -ne 0 ] || [ "$tag" != "v$version" ]; then + while true; do + read -r -p "Increment tag from $tag --> v$version? ([y]/n) " response + if [ -n "$response" ] && ! [[ "$response" =~ ^[NnYy]$ ]]; then + echo "Invalid response." + continue # invalid, so try again + fi + if ! [[ "$response" =~ ^[Nn]$ ]]; then + git tag "v$version" + [ $? -eq 0 ] && echo "Incremented tag from $tag --> v$version" + fi + break + done +fi diff --git a/setup.cfg b/setup.cfg index 1db3624..27197c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,8 +3,8 @@ name = sphinx-automodapi version = attr:sphinx_automodapi.__version__ description = Sphinx extension for auto-generating API documentation for entire modules long_description = file:README.rst -author = The Astropy Developers -author_email = astropy.team@gmail.com +author = Repo from The Astropy Developers, forked by Luke Davis +author_email = astropy.team@gmail.com, lukelbd@gmail.com license = BSD 3-Clause License url = http://astropy.org classifiers = diff --git a/sphinx_automodapi/__init__.py b/sphinx_automodapi/__init__.py index d5b0ac1..cd40119 100644 --- a/sphinx_automodapi/__init__.py +++ b/sphinx_automodapi/__init__.py @@ -1 +1 @@ -__version__ = '0.13.dev0' +__version__ = '0.6.proplot-mods' diff --git a/sphinx_automodapi/automodapi.py b/sphinx_automodapi/automodapi.py index aad1ec1..a4ddadb 100644 --- a/sphinx_automodapi/automodapi.py +++ b/sphinx_automodapi/automodapi.py @@ -434,7 +434,7 @@ def setup(app): from . import automodsumm app.setup_extension(automodsumm.__name__) - app.connect('source-read', process_automodapi) + app.connect('source-read', process_automodapi, priority=100) app.add_config_value('automodapi_inheritance_diagram', True, True) app.add_config_value('automodapi_toctreedirnm', 'api', True) diff --git a/sphinx_automodapi/automodsumm.py b/sphinx_automodapi/automodsumm.py index 61525eb..9a41b38 100644 --- a/sphinx_automodapi/automodsumm.py +++ b/sphinx_automodapi/automodsumm.py @@ -99,6 +99,11 @@ class members that are inherited from a base class. This value can be __all__ = ['Automoddiagram', 'Automodsumm', 'automodsumm_to_autosummary_lines', 'generate_automodsumm_docs', 'process_automodsumm_generation'] +api_ignore_methods = [] +api_class_methods = [ + '__init__', '__call__', # default + '__getitem__', '__setitem__', '__setattr__', '__getattr__', # custom + ] def _str_list_converter(argument): """ @@ -263,13 +268,18 @@ def process_automodsumm_generation(app): f.write(l) f.write('\n') + logger = logging.getLogger(__name__) for sfn, lines in zip(filestosearch, liness): suffix = os.path.splitext(sfn)[1] if len(lines) > 0: - generate_automodsumm_docs( + new_files = generate_automodsumm_docs( lines, sfn, app=app, builder=app.builder, suffix=suffix, base_path=app.srcdir, inherited_members=app.config.automodsumm_inherited_members) + # logger.info('New files: ' + ', '.join(os.path.basename(file) + # for file in new_files)) + for f in new_files: + env.found_docs.add(env.path2doc(f)) # _automodsummrex = re.compile(r'^(\s*)\.\. automodsumm::\s*([A-Za-z0-9_.]+)\s*' @@ -431,10 +441,17 @@ def generate_automodsumm_docs(lines, srcfn, app=None, suffix='.rst', # Create our own templating environment - here we use Astropy's # templates rather than the default autosummary templates, in order to # allow docstrings to be shown for methods. - template_dirs = [os.path.join(os.path.dirname(__file__), 'templates'), - os.path.join(base_path, '_templates')] + local_dir = os.path.join(os.path.dirname(__file__), 'templates') + default_dir = os.path.join(base_path, '_templates') + template_dirs = [local_dir, default_dir] if builder is not None: # allow the user to override the templates + # also add to the templates_path + # TODO: messes up usage for some users? + local_dir_full = os.path.join(local_dir, 'autosummary_core') + templates_path = builder.config.templates_path + if local_dir_full not in templates_path: + templates_path.insert(0, local_dir_full) template_loader = BuiltinTemplateLoader() template_loader.init(builder, dirs=template_dirs) else: @@ -487,11 +504,11 @@ def generate_automodsumm_docs(lines, srcfn, app=None, suffix='.rst', fn = os.path.join(path, name + suffix) # skip it if it exists + # always add to new_files so we can add them to env.found_docs + new_files.append(fn) if os.path.isfile(fn): continue - new_files.append(fn) - f = open(fn, 'w') try: @@ -512,7 +529,14 @@ def get_members_mod(obj, typ, include_public=[]): typ = None -> all """ items = [] + # Add back? Seems fragile to rely on automodapi to ignore + # random variables you declare or classes you import on the + # top-level module. Better to use the :skip: directive *or* + # omit objects from __all__. for name in dir(obj): + # docstring = getattr(safe_getattr(obj, name), '__doc__') + # if not docstring and typ in ('function', 'class', 'exception'): + # continue try: documenter = get_documenter(app, safe_getattr(obj, name), obj) except AttributeError: @@ -523,7 +547,7 @@ def get_members_mod(obj, typ, include_public=[]): if x in include_public or not x.startswith('_')] return public, items - def get_members_class(obj, typ, include_public=[], + def get_members_class(obj, typ, include_public=[], exclude_public=[], include_base=False): """ typ = None -> all @@ -552,19 +576,24 @@ def get_members_class(obj, typ, include_public=[], else: names = getattr(obj, '__dict__').keys() + # Modification here, only document something if it has a + # docstring! Do not inherit if empty, similar to + # :inherited-members: option for name in names: try: documenter = get_documenter(app, safe_getattr(obj, name), obj) except AttributeError: continue + if documenter.objtype == 'method' and not getattr(safe_getattr(obj, name), '__doc__', ''): + continue if typ is None or documenter.objtype == typ: items.append(name) elif typ == 'attribute' and documenter.objtype == 'property': # In Sphinx 2.0 and above, properties have a separate # objtype, but we treat them the same here. items.append(name) - public = [x for x in items - if x in include_public or not x.startswith('_')] + public = [x for x in items if x not in exclude_public and + (x in include_public or not x.startswith('_'))] return public, items ns = {} @@ -585,17 +614,16 @@ def get_members_class(obj, typ, include_public=[], # use default value include_base = inherited_members - api_class_methods = ['__init__', '__call__'] ns['members'] = get_members_class(obj, None, include_base=include_base) ns['methods'], ns['all_methods'] = \ - get_members_class(obj, 'method', api_class_methods, + get_members_class(obj, 'method', api_class_methods, api_ignore_methods, include_base=include_base) ns['attributes'], ns['all_attributes'] = \ get_members_class(obj, 'attribute', include_base=include_base) - ns['methods'].sort() - ns['attributes'].sort() + ns['methods'] = sorted(ns['methods']) + ns['attributes'] = sorted(ns['attributes']) parts = name.split('.') if doc.objtype in ('method', 'attribute'): @@ -649,6 +677,7 @@ def get_members_class(obj, typ, include_public=[], finally: f.close() + return new_files def setup(app): @@ -663,7 +692,7 @@ def setup(app): app.add_directive('automod-diagram', Automoddiagram) app.add_directive('automodsumm', Automodsumm) - app.connect('builder-inited', process_automodsumm_generation) + app.connect('builder-inited', process_automodsumm_generation, priority=100) app.add_config_value('automodsumm_writereprocessed', False, True) app.add_config_value('automodsumm_inherited_members', False, 'env') diff --git a/sphinx_automodapi/templates/autosummary_core/class.rst b/sphinx_automodapi/templates/autosummary_core/class.rst index 85105fa..e6994f8 100644 --- a/sphinx_automodapi/templates/autosummary_core/class.rst +++ b/sphinx_automodapi/templates/autosummary_core/class.rst @@ -20,6 +20,8 @@ .. rubric:: Attributes Summary .. autosummary:: + :toctree: + :template: base.rst {% for item in attributes %} ~{{ name }}.{{ item }} {%- endfor %} @@ -33,6 +35,8 @@ .. rubric:: Methods Summary .. autosummary:: + :toctree: + :template: base.rst {% for item in methods %} ~{{ name }}.{{ item }} {%- endfor %} @@ -40,26 +44,3 @@ {% endif %} {% endblock %} - {% block attributes_documentation %} - {% if attributes %} - - .. rubric:: Attributes Documentation - - {% for item in attributes %} - .. autoattribute:: {{ item }} - {%- endfor %} - - {% endif %} - {% endblock %} - - {% block methods_documentation %} - {% if methods %} - - .. rubric:: Methods Documentation - - {% for item in methods %} - .. automethod:: {{ item }} - {%- endfor %} - - {% endif %} - {% endblock %}