diff --git a/.circleci/config.yml b/.circleci/config.yml index d349db6e0..6ca62abb7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,6 @@ jobs: working_directory: /sphinx steps: - checkout - - run: /python3.5/bin/pip install -U pip setuptools - - run: /python3.5/bin/pip install -U .[test,websupport] - - run: make test PYTHON=/python3.5/bin/python + - run: /python3.6/bin/pip install -U pip setuptools + - run: /python3.6/bin/pip install -U .[test,websupport] + - run: make test PYTHON=/python3.6/bin/python diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 146f3b6bc..0fa6b61fe 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,7 +15,7 @@ Steps to reproduce the behavior: ``` -$ git clone htps://github.com/.../some_project +$ git clone https://github.com/.../some_project $ cd some_project $ pip install -r requirements.txt $ cd docs diff --git a/.travis.yml b/.travis.yml index 5a49bf106..7bc822d70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,6 +37,7 @@ matrix: services: xvfb install: + - "sudo apt-get install graphviz" - if [ $IS_PYTHON = true ]; then pip install -U tox codecov; fi - if [ $IS_PYTHON = false ]; then npm install; fi diff --git a/CHANGES b/CHANGES index 28e99a44d..33cc8ccf7 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,26 @@ +Release 3.0.0 (in development) +============================== + +Dependencies +------------ + +Incompatible changes +-------------------- + +* Drop features and APIs deprecated in 1.8.x + +Deprecated +---------- + +Features added +-------------- + +Bugs fixed +---------- + +Testing +-------- + Release 2.1.0 (in development) ============================== @@ -7,6 +30,67 @@ Dependencies Incompatible changes -------------------- +* Ignore filenames without file extension given to ``Builder.build_specific()`` + API directly + +Deprecated +---------- + +* ``sphinx.builders.latex.LaTeXBuilder.apply_transforms()`` +* ``sphinx.directives.Acks`` +* ``sphinx.directives.Author`` +* ``sphinx.directives.Centered`` +* ``sphinx.directives.Class`` +* ``sphinx.directives.CodeBlock`` +* ``sphinx.directives.Figure`` +* ``sphinx.directives.HList`` +* ``sphinx.directives.Highlight`` +* ``sphinx.directives.Include`` +* ``sphinx.directives.Index`` +* ``sphinx.directives.LiteralInclude`` +* ``sphinx.directives.Meta`` +* ``sphinx.directives.Only`` +* ``sphinx.directives.SeeAlso`` +* ``sphinx.directives.TabularColumns`` +* ``sphinx.directives.TocTree`` +* ``sphinx.directives.VersionChange`` +* ``sphinx.environment.NoUri`` +* ``sphinx.ext.autodoc.importer.MockFinder`` +* ``sphinx.ext.autodoc.importer.MockLoader`` +* ``sphinx.ext.autodoc.importer.mock()`` +* ``sphinx.ext.autosummary.autolink_role()`` +* ``sphinx.util.docfields.DocFieldTransformer.preprocess_fieldtypes()`` +* ``sphinx.util.node.find_source_node()`` +* ``sphinx.util.i18n.find_catalog()`` +* ``sphinx.util.i18n.find_catalog_files()`` +* ``sphinx.util.i18n.find_catalog_source_files()`` + +For more details, see :ref:`deprecation APIs list `. + +Features added +-------------- + +* Add a helper class ``sphinx.transforms.post_transforms.SphinxPostTransform`` +* Add a helper method ``SphinxDirective.set_source_info()`` +* #6180: Support ``--keep-going`` with BuildDoc setup command +* ``math`` directive now supports ``:class:`` option +* todo: ``todo`` directive now supports ``:name:`` option + +Bugs fixed +---------- + +Testing +-------- + +Release 2.0.1 (in development) +============================== + +Dependencies +------------ + +Incompatible changes +-------------------- + Deprecated ---------- @@ -16,36 +100,19 @@ Features added Bugs fixed ---------- -Testing --------- - -Release 2.0.0 beta2 (in development) -==================================== - -Dependencies ------------- - -Incompatible changes --------------------- - -Deprecated ----------- - -Features added --------------- - -Bugs fixed ----------- +* LaTeX: some system labels are not translated Testing -------- -Release 2.0.0 beta1 (in development) -==================================== +Release 2.0.0 (released Mar 29, 2019) +===================================== Dependencies ------------ +2.0.0b1 + * LaTeX builder now depends on TeX Live 2015 or above. * LaTeX builder (with ``'pdflatex'`` :confval:`latex_engine`) will process Unicode Greek letters in text (not in math mark-up) via the text font and @@ -73,8 +140,11 @@ Dependencies Incompatible changes -------------------- +2.0.0b1 + * Drop python 2.7 and 3.4 support * Drop docutils 0.11 support +* Drop features and APIs deprecated in 1.7.x * The default setting for :confval:`master_doc` is changed to ``'index'`` which has been longly used as default of sphinx-quickstart. * LaTeX: Move message resources to ``sphinxmessage.sty`` @@ -115,9 +185,15 @@ Incompatible changes * #4550: All tables and figures without ``align`` option are displayed to center * #4587: html: Output HTML5 by default +2.0.0b2 + +* texinfo: image files are copied into ``name-figure`` directory + Deprecated ---------- +2.0.0b1 + * Support for evaluating Python 2 syntax is deprecated. This includes configuration files which should be converted to Python 3. * The arguments of ``EpubBuilder.build_mimetype()``, @@ -205,6 +281,8 @@ For more details, see :ref:`deprecation APIs list `. Features added -------------- +2.0.0b1 + * #1618: The search results preview of generated HTML documentation is reader-friendlier: instead of showing the snippets as raw reStructuredText markup, Sphinx now renders the corresponding HTML. This means the Sphinx @@ -254,6 +332,8 @@ Features added Bugs fixed ---------- +2.0.0b1 + * #1682: LaTeX: writer should not translate Greek unicode, but use textgreek package * #5247: LaTeX: PDF does not build with default font config for Russian @@ -275,25 +355,44 @@ Bugs fixed * HTML search: search always returns nothing when multiple search terms are used and one term is shorter than three characters +2.0.0b2 + +* #6096: html: Anchor links are not added to figures +* #3620: html: Defer searchindex.js rather than loading it via ajax +* #6113: html: Table cells and list items have large margins +* #5508: ``linenothreshold`` option for ``highlight`` directive was ignored +* texinfo: ``make install-info`` causes syntax error +* texinfo: ``make install-info`` fails on macOS +* #3079: texinfo: image files are not copied on ``make install-info`` +* #5391: A cross reference in heading is rendered as literal +* #5946: C++, fix ``cpp:alias`` problems in LaTeX (and singlehtml) +* #6147: classes attribute of ``citation_reference`` node is lost +* AssertionError is raised when custom ``citation_reference`` node having + classes attribute refers missing citation (refs: #6147) +* #2155: Support ``code`` directive +* C++, fix parsing of braced initializers. +* #6172: AttributeError is raised for old styled index nodes +* #4872: inheritance_diagram: correctly describe behavior of ``parts`` option in + docs, allow negative values. +* #6178: i18n: Captions missing in translations for hidden TOCs + +2.0.0 final + +* #6196: py domain: unexpected prefix is generated + Testing -------- +2.0.0b1 + * Stop to use ``SPHINX_TEST_TEMPDIR`` envvar -Release 1.8.5 (in development) -============================== +2.0.0b2 -Dependencies ------------- +* Add a helper function: ``sphinx.testing.restructuredtext.parse()`` -Incompatible changes --------------------- - -Deprecated ----------- - -Features added --------------- +Release 1.8.5 (released Mar 10, 2019) +===================================== Bugs fixed ---------- @@ -301,11 +400,18 @@ Bugs fixed * LaTeX: Remove extraneous space after author names on PDF title page (refs: #6004) * #6026: LaTeX: A cross reference to definition list does not work * #6046: LaTeX: ``TypeError`` is raised when invalid latex_elements given +* #6067: LaTeX: images having a target are concatenated to next line +* #6067: LaTeX: images having a target are not aligned even if specified +* #6149: LaTeX: ``:index:`` role in titles causes ``Use of \@icentercr doesn't + match its definition`` error on latexpdf build * #6019: imgconverter: Including multipage PDF fails * #6047: autodoc: ``autofunction`` emits a warning for method objects - -Testing --------- +* #6028: graphviz: Ensure the graphviz filenames are reproducible +* #6068: doctest: ``skipif`` option may remove the code block from documentation +* #6136: ``:name:`` option for ``math`` directive causes a crash +* #6139: intersphinx: ValueError on failure reporting +* #6135: changes: Fix UnboundLocalError when any module found +* #3859: manpage: code-block captions are not displayed correctly Release 1.8.4 (released Feb 03, 2019) ===================================== diff --git a/EXAMPLES b/EXAMPLES index 86b153812..989c0f969 100644 --- a/EXAMPLES +++ b/EXAMPLES @@ -206,6 +206,7 @@ Documentation using sphinx_rtd_theme * `Jupyter Notebook `__ * `Lasagne `__ * `latexindent.pl `__ +* `Learning Apache Spark with Python `__ * `Linguistica `__ * `Linux kernel `__ * `MathJax `__ @@ -240,6 +241,7 @@ Documentation using sphinx_rtd_theme * `Releases Sphinx extension `__ * `Qtile `__ * `Quex `__ +* `QuTiP `__ * `Satchmo `__ * `Scapy `__ * `SimGrid `__ @@ -251,6 +253,7 @@ Documentation using sphinx_rtd_theme * `Sphinx AutoAPI `__ * `sphinx-argparse `__ * `Sphinx-Gallery `__ (customized) +* `Sphinx with Github Webpages `__ * `SpotBugs `__ * `StarUML `__ * `Sublime Text Unofficial Documentation `__ diff --git a/babel.cfg b/babel.cfg index a57e0ac09..d41d26ad0 100644 --- a/babel.cfg +++ b/babel.cfg @@ -14,6 +14,13 @@ variable_end_string = %> block_start_string = <% block_end_string = %> +# Extraction from Jinja2 template files +[jinja2: **/templates/latex/**.sty_t] +variable_start_string = <%= +variable_end_string = %> +block_start_string = <% +block_end_string = %> + # Extraction from Jinja2 HTML templates [jinja2: **/themes/**.html] encoding = utf-8 diff --git a/doc/conf.py b/doc/conf.py index 58cbfe708..16594f038 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -7,7 +7,7 @@ import sphinx extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.autosummary', 'sphinx.ext.extlinks', - 'sphinx.ext.viewcode'] + 'sphinx.ext.viewcode', 'sphinx.ext.inheritance_diagram'] master_doc = 'contents' templates_path = ['_templates'] @@ -48,7 +48,7 @@ epub_fix_images = False epub_max_image_width = 0 epub_show_urls = 'inline' epub_use_index = False -epub_guide = (('toc', 'contents.xhtml', u'Table of Contents'),) +epub_guide = (('toc', 'contents.xhtml', 'Table of Contents'),) epub_description = 'Sphinx documentation generator system manual' latex_documents = [('contents', 'sphinx.tex', 'Sphinx Documentation', diff --git a/doc/development/tutorials/examples/README.rst b/doc/development/tutorials/examples/README.rst new file mode 100644 index 000000000..2b9c01b5e --- /dev/null +++ b/doc/development/tutorials/examples/README.rst @@ -0,0 +1,11 @@ +:orphan: + +Tutorial examples +================= + +This directory contains a number of examples used in the tutorials. These are +intended to be increasingly complex to demonstrate the various features of +Sphinx, but should aim to be as complicated as necessary but no more. +Individual sections are referenced by line numbers, meaning if you make changes +to the source files, you should update the references in the documentation +accordingly. diff --git a/doc/development/tutorials/examples/helloworld.py b/doc/development/tutorials/examples/helloworld.py new file mode 100644 index 000000000..d6d81fd4f --- /dev/null +++ b/doc/development/tutorials/examples/helloworld.py @@ -0,0 +1,19 @@ +from docutils import nodes +from docutils.parsers.rst import Directive + + +class HelloWorld(Directive): + + def run(self): + paragraph_node = nodes.paragraph(text='Hello World!') + return [paragraph_node] + + +def setup(app): + app.add_directive("helloworld", HelloWorld) + + return { + 'version': '0.1', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/doc/development/tutorials/examples/recipe.py b/doc/development/tutorials/examples/recipe.py new file mode 100644 index 000000000..2464302da --- /dev/null +++ b/doc/development/tutorials/examples/recipe.py @@ -0,0 +1,161 @@ +from collections import defaultdict + +from docutils.parsers.rst import directives + +from sphinx import addnodes +from sphinx.directives import ObjectDescription +from sphinx.domains import Domain +from sphinx.domains import Index +from sphinx.roles import XRefRole +from sphinx.util.nodes import make_refnode + + +class RecipeDirective(ObjectDescription): + """A custom directive that describes a recipe.""" + + has_content = True + required_arguments = 1 + option_spec = { + 'contains': directives.unchanged_required, + } + + def handle_signature(self, sig, signode): + signode += addnodes.desc_name(text=sig) + return sig + + def add_target_and_index(self, name_cls, sig, signode): + signode['ids'].append('recipe' + '-' + sig) + if 'noindex' not in self.options: + ingredients = [ + x.strip() for x in self.options.get('contains').split(',')] + + recipes = self.env.get_domain('recipe') + recipes.add_recipe(sig, ingredients) + + +class IngredientIndex(Index): + """A custom index that creates an ingredient matrix.""" + + name = 'ingredient' + localname = 'Ingredient Index' + shortname = 'Ingredient' + + def generate(self, docnames=None): + content = defaultdict(list) + + recipes = {name: (dispname, typ, docname, anchor) + for name, dispname, typ, docname, anchor, _ + in self.domain.get_objects()} + recipe_ingredients = self.domain.data['recipe_ingredients'] + ingredient_recipes = defaultdict(list) + + # flip from recipe_ingredients to ingredient_recipes + for recipe_name, ingredients in recipe_ingredients.items(): + for ingredient in ingredients: + ingredient_recipes[ingredient].append(recipe_name) + + # convert the mapping of ingredient to recipes to produce the expected + # output, shown below, using the ingredient name as a key to group + # + # name, subtype, docname, anchor, extra, qualifier, description + for ingredient, recipe_names in ingredient_recipes.items(): + for recipe_name in recipe_names: + dispname, typ, docname, anchor = recipes[recipe_name] + content[ingredient].append( + (dispname, 0, docname, anchor, docname, '', typ)) + + # convert the dict to the sorted list of tuples expected + content = sorted(content.items()) + + return content, True + + +class RecipeIndex(Index): + """A custom index that creates an recipe matrix.""" + + name = 'recipe' + localname = 'Recipe Index' + shortname = 'Recipe' + + def generate(self, docnames=None): + content = defaultdict(list) + + # sort the list of recipes in alphabetical order + recipes = self.domain.get_objects() + recipes = sorted(recipes, key=lambda recipe: recipe[0]) + + # generate the expected output, shown below, from the above using the + # first letter of the recipe as a key to group thing + # + # name, subtype, docname, anchor, extra, qualifier, description + for name, dispname, typ, docname, anchor, _ in recipes: + content[dispname[0].lower()].append( + (dispname, 0, docname, anchor, docname, '', typ)) + + # convert the dict to the sorted list of tuples expected + content = sorted(content.items()) + + return content, True + + +class RecipeDomain(Domain): + + name = 'recipe' + label = 'Recipe Sample' + roles = { + 'ref': XRefRole() + } + directives = { + 'recipe': RecipeDirective, + } + indices = { + RecipeIndex, + IngredientIndex + } + initial_data = { + 'recipes': [], # object list + 'recipe_ingredients': {}, # name -> object + } + + def get_full_qualified_name(self, node): + return '{}.{}'.format('recipe', node.arguments[0]) + + def get_objects(self): + for obj in self.data['recipes']: + yield(obj) + + def resolve_xref(self, env, fromdocname, builder, typ, target, node, + contnode): + match = [(docname, anchor) + for name, sig, typ, docname, anchor, prio + in self.get_objects() if sig == target] + + if len(match) > 0: + todocname = match[0][0] + targ = match[0][1] + + return make_refnode(builder, fromdocname, todocname, targ, + contnode, targ) + else: + print('Awww, found nothing') + return None + + def add_recipe(self, signature, ingredients): + """Add a new recipe to the domain.""" + name = '{}.{}'.format('recipe', signature) + anchor = 'recipe-{}'.format(signature) + + self.data['recipe_ingredients'][name] = ingredients + # name, dispname, type, docname, anchor, priority + self.data['recipes'].append( + (name, signature, 'Recipe', self.env.docname, anchor, 0)) + + +def setup(app): + app.add_domain(RecipeDomain) + + return { + 'version': '0.1', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/doc/development/tutorials/examples/todo.py b/doc/development/tutorials/examples/todo.py new file mode 100644 index 000000000..6ba39944f --- /dev/null +++ b/doc/development/tutorials/examples/todo.py @@ -0,0 +1,124 @@ +from docutils import nodes +from docutils.parsers.rst import Directive + +from sphinx.locale import _ +from sphinx.util.docutils import SphinxDirective + + +class todo(nodes.Admonition, nodes.Element): + pass + + +class todolist(nodes.General, nodes.Element): + pass + + +def visit_todo_node(self, node): + self.visit_admonition(node) + + +def depart_todo_node(self, node): + self.depart_admonition(node) + + +class TodolistDirective(Directive): + + def run(self): + return [todolist('')] + + +class TodoDirective(SphinxDirective): + + # this enables content in the directive + has_content = True + + def run(self): + targetid = 'todo-%d' % self.env.new_serialno('todo') + targetnode = nodes.target('', '', ids=[targetid]) + + todo_node = todo('\n'.join(self.content)) + todo_node += nodes.title(_('Todo'), _('Todo')) + self.state.nested_parse(self.content, self.content_offset, todo_node) + + if not hasattr(self.env, 'todo_all_todos'): + self.env.todo_all_todos = [] + + self.env.todo_all_todos.append({ + 'docname': self.env.docname, + 'lineno': self.lineno, + 'todo': todo_node.deepcopy(), + 'target': targetnode, + }) + + return [targetnode, todo_node] + + +def purge_todos(app, env, docname): + if not hasattr(env, 'todo_all_todos'): + return + + env.todo_all_todos = [todo for todo in env.todo_all_todos + if todo['docname'] != docname] + + +def process_todo_nodes(app, doctree, fromdocname): + if not app.config.todo_include_todos: + for node in doctree.traverse(todo): + node.parent.remove(node) + + # Replace all todolist nodes with a list of the collected todos. + # Augment each todo with a backlink to the original location. + env = app.builder.env + + for node in doctree.traverse(todolist): + if not app.config.todo_include_todos: + node.replace_self([]) + continue + + content = [] + + for todo_info in env.todo_all_todos: + para = nodes.paragraph() + filename = env.doc2path(todo_info['docname'], base=None) + description = ( + _('(The original entry is located in %s, line %d and can be found ') % + (filename, todo_info['lineno'])) + para += nodes.Text(description, description) + + # Create a reference + newnode = nodes.reference('', '') + innernode = nodes.emphasis(_('here'), _('here')) + newnode['refdocname'] = todo_info['docname'] + newnode['refuri'] = app.builder.get_relative_uri( + fromdocname, todo_info['docname']) + newnode['refuri'] += '#' + todo_info['target']['refid'] + newnode.append(innernode) + para += newnode + para += nodes.Text('.)', '.)') + + # Insert into the todolist + content.append(todo_info['todo']) + content.append(para) + + node.replace_self(content) + + +def setup(app): + app.add_config_value('todo_include_todos', False, 'html') + + app.add_node(todolist) + app.add_node(todo, + html=(visit_todo_node, depart_todo_node), + latex=(visit_todo_node, depart_todo_node), + text=(visit_todo_node, depart_todo_node)) + + app.add_directive('todo', TodoDirective) + app.add_directive('todolist', TodolistDirective) + app.connect('doctree-resolved', process_todo_nodes) + app.connect('env-purge-doc', purge_todos) + + return { + 'version': '0.1', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/doc/development/tutorials/helloworld.rst b/doc/development/tutorials/helloworld.rst index 5ce8db66c..a042f7b05 100644 --- a/doc/development/tutorials/helloworld.rst +++ b/doc/development/tutorials/helloworld.rst @@ -1,85 +1,89 @@ -Developing a "Hello world" directive +Developing a "Hello world" extension ==================================== -The objective of this tutorial is to create a very basic extension that adds a new -directive that outputs a paragraph containing `hello world`. +The objective of this tutorial is to create a very basic extension that adds a +new directive. This directive will output a paragraph containing "hello world". -Only basic information is provided in this tutorial. For more information, -refer to the :doc:`other tutorials ` that go into more -details. +Only basic information is provided in this tutorial. For more information, refer +to the :doc:`other tutorials ` that go into more details. -.. warning:: For this extension, you will need some basic understanding of docutils_ +.. warning:: + + For this extension, you will need some basic understanding of docutils_ and Python. -Creating a new extension file ------------------------------ -Your extension file could be in any folder of your project. In our case, -let's do the following: +Overview +-------- -#. Create an :file:`_ext` folder in :file:`source`. +We want the extension to add the following to Sphinx: + +* A ``helloworld`` directive, that will simply output the text "hello world". + + +Prerequisites +------------- + +We will not be distributing this plugin via `PyPI`_ and will instead include it +as part of an existing project. This means you will need to use an existing +project or create a new one using :program:`sphinx-quickstart`. + +We assume you are using separate source (:file:`source`) and build +(:file:`build`) folders. Your extension file could be in any folder of your +project. In our case, let's do the following: + +#. Create an :file:`_ext` folder in :file:`source` #. Create a new Python file in the :file:`_ext` folder called - :file:`helloworld.py`. + :file:`helloworld.py` - Here is an example of the folder structure you might obtain: +Here is an example of the folder structure you might obtain: - .. code-block:: text +.. code-block:: text + + └── source +    ├── _ext + │   └── helloworld.py +    ├── _static +    ├── conf.py +    ├── somefolder +    ├── index.rst +    ├── somefile.rst +    └── someotherfile.rst - └── source -    ├── _ext - │   └── helloworld.py -    ├── _static -    ├── _themes -    ├── conf.py -    ├── somefolder -    ├── somefile.rst -    └── someotherfile.rst Writing the extension --------------------- Open :file:`helloworld.py` and paste the following code in it: -.. code-block:: python +.. literalinclude:: examples/helloworld.py + :language: python + :linenos: - from docutils import nodes - from docutils.parsers.rst import Directive +Some essential things are happening in this example, and you will see them for +all directives. +.. rubric:: The directive class - class HelloWorld(Directive): - def run(self): - paragraph_node = nodes.paragraph(text='Hello World!') - return [paragraph_node] +Our new directive is declared in the ``HelloWorld`` class. +.. literalinclude:: examples/helloworld.py + :language: python + :linenos: + :lines: 5-9 - def setup(app): - app.add_directive("helloworld", HelloWorld) - - -Some essential things are happening in this example, and you will see them -in all directives: - -.. rubric:: Directive declaration - -Our new directive is declared in the ``HelloWorld`` class, it extends -docutils_' ``Directive`` class. All extensions that create directives -should extend this class. - -.. rubric:: ``run`` method - -This method is a requirement and it is part of every directive. It contains -the main logic of the directive and it returns a list of docutils nodes to -be processed by Sphinx. +This class extends the docutils_' ``Directive`` class. All extensions that +create directives should extend this class. .. seealso:: - :doc:`todo` + `The docutils documentation on creating directives `_ -.. rubric:: docutils nodes - -The ``run`` method returns a list of nodes. Nodes are docutils' way of -representing the content of a document. There are many types of nodes -available: text, paragraph, reference, table, etc. +This class contains a ``run`` method. This method is a requirement and it is +part of every directive. It contains the main logic of the directive and it +returns a list of docutils nodes to be processed by Sphinx. These nodes are +docutils' way of representing the content of a document. There are many types of +nodes available: text, paragraph, reference, table, etc. .. seealso:: @@ -89,74 +93,97 @@ The ``nodes.paragraph`` class creates a new paragraph node. A paragraph node typically contains some text that we can set during instantiation using the ``text`` parameter. -.. rubric:: ``setup`` function +.. rubric:: The ``setup`` function + +.. currentmodule:: sphinx.application This function is a requirement. We use it to plug our new directive into Sphinx. -The simplest thing you can do it call the ``app.add_directive`` method. -.. note:: +.. literalinclude:: examples/helloworld.py + :language: python + :linenos: + :lines: 12- - The first argument is the name of the directive itself as used in an rST file. +The simplest thing you can do it call the :meth:`~Sphinx.add_directive` method, +which is what we've done here. For this particular call, the first argument is +the name of the directive itself as used in a reST file. In this case, we would +use ``helloworld``. For example: - In our case, we would use ``helloworld``: +.. code-block:: rst - .. code-block:: rst + Some intro text here... - Some intro text here... + .. helloworld:: - .. helloworld:: + Some more text here... - Some more text here... +We also return the :ref:`extension metadata ` that indicates the +version of our extension, along with the fact that it is safe to use the +extension for both parallel reading and writing. -Updating the conf.py file -------------------------- +Using the extension +------------------- -The extension file has to be declared in your :file:`conf.py` file to make -Sphinx aware of it: +The extension has to be declared in your :file:`conf.py` file to make Sphinx +aware of it. There are two steps necessary here: -#. Open :file:`conf.py`. It is in the :file:`source` folder by default. -#. Add ``sys.path.append(os.path.abspath("./_ext"))`` before - the ``extensions`` variable declaration (if it exists). -#. Update or create the ``extensions`` list and add the - extension file name to the list: +#. Add the :file:`_ext` directory to the `Python path`_ using + ``sys.path.append``. This should be placed at the top of the file. - .. code-block:: python +#. Update or create the :confval:`extensions` list and add the extension file + name to the list - extensions.append('helloworld') +For example: -You can now use the extension. +.. code-block:: python -.. admonition:: Example + import os + import sys - .. code-block:: rst + sys.path.append(os.path.abspath("./_ext")) - Some intro text here... + extensions = ['helloworld'] - .. helloworld:: +.. tip:: - Some more text here... + We're not distributing this extension as a `Python package`_, we need to + modify the `Python path`_ so Sphinx can find our extension. This is why we + need the call to ``sys.path.append``. - The sample above would generate: +You can now use the extension in a file. For example: - .. code-block:: text +.. code-block:: rst - Some intro text here... + Some intro text here... - Hello World! + .. helloworld:: - Some more text here... + Some more text here... + +The sample above would generate: + +.. code-block:: text + + Some intro text here... + + Hello World! + + Some more text here... + + +Further reading +--------------- This is the very basic principle of an extension that creates a new directive. For a more advanced example, refer to :doc:`todo`. -Further reading ---------------- - -You can create your own nodes if needed, refer to the -:doc:`todo` for more information. .. _docutils: http://docutils.sourceforge.net/ -.. _`docutils nodes`: http://docutils.sourceforge.net/docs/ref/doctree.html \ No newline at end of file +.. _docutils directives: http://docutils.sourceforge.net/docs/howto/rst-directives.html +.. _docutils nodes: http://docutils.sourceforge.net/docs/ref/doctree.html +.. _PyPI: https://pypi.org/ +.. _Python package: https://packaging.python.org/ +.. _Python path: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH diff --git a/doc/development/tutorials/index.rst b/doc/development/tutorials/index.rst index cb8dce435..a79e6a8b6 100644 --- a/doc/development/tutorials/index.rst +++ b/doc/development/tutorials/index.rst @@ -9,3 +9,4 @@ Refer to the following tutorials to get started with extension development. helloworld todo + recipe diff --git a/doc/development/tutorials/recipe.rst b/doc/development/tutorials/recipe.rst new file mode 100644 index 000000000..2a3aa6408 --- /dev/null +++ b/doc/development/tutorials/recipe.rst @@ -0,0 +1,217 @@ +Developing a "recipe" extension +=============================== + +The objective of this tutorial is to illustrate roles, directives and domains. +Once complete, we will be able to use this extension to describe a recipe and +reference that recipe from elsewhere in our documentation. + +.. note:: + + This tutorial is based on a guide first published on `opensource.com`_ and + is provided here with the original author's permission. + + .. _opensource.com: https://opensource.com/article/18/11/building-custom-workflows-sphinx + + +Overview +-------- + +We want the extension to add the following to Sphinx: + +* A ``recipe`` :term:`directive`, containing some content describing the recipe + steps, along with a ``:contains:`` option highlighting the main ingredients + of the recipe. + +* A ``ref`` :term:`role`, which provides a cross-reference to the recipe + itself. + +* A ``recipe`` :term:`domain`, which allows us to tie together the above role + and domain, along with things like indices. + +For that, we will need to add the following elements to Sphinx: + +* A new directive called ``recipe`` + +* New indexes to allow us to reference ingredient and recipes + +* A new domain called ``recipe``, which will contain the ``recipe`` directive + and ``ref`` role + + +Prerequisites +------------- + +We need the same setup as in :doc:`the previous extensions `. This time, +we will be putting out extension in a file called :file:`recipe.py`. + +Here is an example of the folder structure you might obtain: + +.. code-block:: text + + └── source +    ├── _ext + │   └── recipe.py +    ├── conf.py +    └── index.rst + + +Writing the extension +--------------------- + +Open :file:`recipe.py` and paste the following code in it, all of which we will +explain in detail shortly: + +.. literalinclude:: examples/recipe.py + :language: python + :linenos: + +Let's look at each piece of this extension step-by-step to explain what's going +on. + +.. rubric:: The directive class + +The first thing to examine is the ``RecipeDirective`` directive: + +.. literalinclude:: examples/recipe.py + :language: python + :linenos: + :lines: 17-37 + +Unlike :doc:`helloworld` and :doc:`todo`, this directive doesn't derive from +:class:`docutils.parsers.rst.Directive` and doesn't define a ``run`` method. +Instead, it derives from :class:`sphinx.directives.ObjectDescription` and +defines ``handle_signature`` and ``add_target_and_index`` methods. This is +because ``ObjectDescription`` is a special-purpose directive that's intended +for describing things like classes, functions, or, in our case, recipes. More +specifically, ``handle_signature`` implements parsing the signature of the +directive and passes on the object's name and type to its superclass, while +``add_taget_and_index`` adds a target (to link to) and an entry to the index +for this node. + +We also see that this directive defines ``has_content``, ``required_arguments`` +and ``option_spec``. Unlike the ``TodoDirective`` directive added in the +:doc:`previous tutorial `, this directive takes a single argument, the +recipe name, and an option, ``contains``, in addition to the nested +reStructuredText in the body. + +.. rubric:: The index classes + +.. currentmodule:: sphinx.domains + +.. todo:: Add brief overview of indices + +.. literalinclude:: examples/recipe.py + :language: python + :linenos: + :lines: 40-102 + +Both ``IngredientIndex`` and ``RecipeIndex`` are derived from :class:`Index`. +They implement custom logic to generate a tuple of values that define the +index. Note that ``RecipeIndex`` is a simple index that has only one entry. +Extending it to cover more object types is not yet part of the code. + +Both indices use the method :meth:`Index.generate` to do their work. This +method combines the information from our domain, sorts it, and returns it in a +list structure that will be accepted by Sphinx. This might look complicated but +all it really is is a list of tuples like ``('tomato', 'TomatoSoup', 'test', +'rec-TomatoSoup',...)``. Refer to the :doc:`domain API guide +` for more information on this API. + +.. rubric:: The domain + +A Sphinx domain is a specialized container that ties together roles, +directives, and indices, among other things. Let's look at the domain we're +creating here. + +.. literalinclude:: examples/recipe.py + :language: python + :linenos: + :lines: 105-155 + +There are some interesting things to note about this ``recipe`` domain and domains +in general. Firstly, we actually register our directives, roles and indices +here, via the ``directives``, ``roles`` and ``indices`` attributes, rather than +via calls later on in ``setup``. We can also note that we aren't actually +defining a custom role and are instead reusing the +:class:`sphinx.roles.XRefRole` role and defining the +:class:`sphinx.domains.Domain.resolve_xref` method. This method takes two +arguments, ``typ`` and ``target``, which refer to the cross-reference type and +its target name. We'll use ``target`` to resolve our destination from our +domain's ``recipes`` because we currently have only one type of node. + +Moving on, we can see that we've defined ``initial_data``. The values defined in +``initial_data`` will be copied to ``env.domaindata[domain_name]`` as the +initial data of the domain, and domain instances can access it via +``self.data``. We see that we have defined two items in ``initial_data``: +``recipes`` and ``recipe2ingredient``. These contain a list of all objects +defined (i.e. all recipes) and a hash that maps a canonical ingredient name to +the list of objects. The way we name objects is common across our extension and +is defined in the ``get_full_qualified_name`` method. For each object created, +the canonical name is ``recipe.``, where ```` is the +name the documentation writer gives the object (a recipe). This enables the +extension to use different object types that share the same name. Having a +canonical name and central place for our objects is a huge advantage. Both our +indices and our cross-referencing code use this feature. + +.. rubric:: The ``setup`` function + +.. currentmodule:: sphinx.application + +:doc:`As always `, the ``setup`` function is a requirement and is used to +hook the various parts of our extension into Sphinx. Let's look at the +``setup`` function for this extension. + +.. literalinclude:: examples/recipe.py + :language: python + :linenos: + :lines: 158- + +This looks a little different to what we're used to seeing. There are no calls +to :meth:`~Sphinx.add_directive` or even :meth:`~Sphinx.add_role`. Instead, we +have a single call to :meth:`~Sphinx.add_domain` followed by some +initialization of the :ref:`standard domain `. This is because we +had already registered our directives, roles and indexes as part of the +directive itself. + + +Using the extension +------------------- + +You can now use the extension throughout your project. For example: + +.. code-block:: rst + :caption: index.rst + + Joe's Recipes + ============= + + Below are a collection of my favourite recipes. I highly recommend the + :recipe:ref:`TomatoSoup` recipe in particular! + + .. toctree:: + + tomato-soup + +.. code-block:: rst + :caption: tomato-soup.rst + + The recipe contains `tomato` and `cilantro`. + + .. recipe:recipe:: TomatoSoup + :contains: tomato cilantro salt pepper + + This recipe is a tasty tomato soup, combine all ingredients + and cook. + +The important things to note are the use of the ``:recipe:ref:`` role to +cross-reference the recipe actually defined elsewhere (using the +``:recipe:recipe:`` directive. + + +Further reading +--------------- + +For more information, refer to the `docutils`_ documentation and +:doc:`/extdev/index`. + +.. _docutils: http://docutils.sourceforge.net/docs/ diff --git a/doc/development/tutorials/todo.rst b/doc/development/tutorials/todo.rst index e68a39342..78a37c2fe 100644 --- a/doc/development/tutorials/todo.rst +++ b/doc/development/tutorials/todo.rst @@ -1,114 +1,99 @@ Developing a "TODO" extension ============================= -This section is intended as a walkthrough for the creation of custom extensions. -It covers the basics of writing and activating an extension, as well as -commonly used features of extensions. - -As an example, we will cover a "todo" extension that adds capabilities to -include todo entries in the documentation, and to collect these in a central -place. (A similar "todo" extension is distributed with Sphinx.) +The objective of this tutorial is to create a more comprehensive extension than +that created in :doc:`helloworld`. Whereas that guide just covered writing a +custom :term:`directive`, this guide adds multiple directives, along with custom +nodes, additional config values and custom event handlers. To this end, we will +cover a ``todo`` extension that adds capabilities to include todo entries in the +documentation, and to collect these in a central place. This is similar the +``sphinxext.todo`` extension distributed with Sphinx. -Extension Design ----------------- +Overview +-------- -.. note:: To understand the design this extension, refer to +.. note:: + To understand the design of this extension, refer to :ref:`important-objects` and :ref:`build-phases`. We want the extension to add the following to Sphinx: -* A "todo" directive, containing some content that is marked with "TODO", and - only shown in the output if a new config value is set. (Todo entries should - not be in the output by default.) +* A ``todo`` directive, containing some content that is marked with "TODO" and + only shown in the output if a new config value is set. Todo entries should not + be in the output by default. -* A "todolist" directive that creates a list of all todo entries throughout the +* A ``todolist`` directive that creates a list of all todo entries throughout the documentation. For that, we will need to add the following elements to Sphinx: * New directives, called ``todo`` and ``todolist``. + * New document tree nodes to represent these directives, conventionally also called ``todo`` and ``todolist``. We wouldn't need new nodes if the new directives only produced some content representable by existing nodes. + * A new config value ``todo_include_todos`` (config value names should start with the extension name, in order to stay unique) that controls whether todo entries make it into the output. + * New event handlers: one for the :event:`doctree-resolved` event, to replace the todo and todolist nodes, and one for :event:`env-purge-doc` (the reason for that will be covered later). -The Setup Function ------------------- +Prerequisites +------------- -.. currentmodule:: sphinx.application +As with :doc:`helloworld`, we will not be distributing this plugin via PyPI so +once again we need a Sphinx project to call this from. You can use an existing +project or create a new one using :program:`sphinx-quickstart`. -The new elements are added in the extension's setup function. Let us create a -new Python module called :file:`todo.py` and add the setup function:: +We assume you are using separate source (:file:`source`) and build +(:file:`build`) folders. Your extension file could be in any folder of your +project. In our case, let's do the following: - def setup(app): - app.add_config_value('todo_include_todos', False, 'html') +#. Create an :file:`_ext` folder in :file:`source` +#. Create a new Python file in the :file:`_ext` folder called :file:`todo.py` - app.add_node(todolist) - app.add_node(todo, - html=(visit_todo_node, depart_todo_node), - latex=(visit_todo_node, depart_todo_node), - text=(visit_todo_node, depart_todo_node)) +Here is an example of the folder structure you might obtain: - app.add_directive('todo', TodoDirective) - app.add_directive('todolist', TodolistDirective) - app.connect('doctree-resolved', process_todo_nodes) - app.connect('env-purge-doc', purge_todos) +.. code-block:: text - return {'version': '0.1'} # identifies the version of our extension - -The calls in this function refer to classes and functions not yet written. What -the individual calls do is the following: - -* :meth:`~Sphinx.add_config_value` lets Sphinx know that it should recognize the - new *config value* ``todo_include_todos``, whose default value should be - ``False`` (this also tells Sphinx that it is a boolean value). - - If the third argument was ``'html'``, HTML documents would be full rebuild if the - config value changed its value. This is needed for config values that - influence reading (build :ref:`phase 1 `). - -* :meth:`~Sphinx.add_node` adds a new *node class* to the build system. It also - can specify visitor functions for each supported output format. These visitor - functions are needed when the new nodes stay until :ref:`phase 4 ` - -- since the ``todolist`` node is always replaced in :ref:`phase 3 `, - it doesn't need any. - - We need to create the two node classes ``todo`` and ``todolist`` later. - -* :meth:`~Sphinx.add_directive` adds a new *directive*, given by name and class. - - The handler functions are created later. - -* Finally, :meth:`~Sphinx.connect` adds an *event handler* to the event whose - name is given by the first argument. The event handler function is called - with several arguments which are documented with the event. + └── source +    ├── _ext + │   └── todo.py +    ├── _static +    ├── conf.py +    ├── somefolder +    ├── index.rst +    ├── somefile.rst +    └── someotherfile.rst -The Node Classes ----------------- +Writing the extension +--------------------- -Let's start with the node classes:: +Open :file:`todo.py` and paste the following code in it, all of which we will +explain in detail shortly: - from docutils import nodes +.. literalinclude:: examples/todo.py + :language: python + :linenos: - class todo(nodes.Admonition, nodes.Element): - pass +This is far more extensive extension than the one detailed in :doc:`helloworld`, +however, we will will look at each piece step-by-step to explain what's +happening. - class todolist(nodes.General, nodes.Element): - pass +.. rubric:: The node classes - def visit_todo_node(self, node): - self.visit_admonition(node) +Let's start with the node classes: - def depart_todo_node(self, node): - self.depart_admonition(node) +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 8-21 Node classes usually don't have to do anything except inherit from the standard docutils classes defined in :mod:`docutils.nodes`. ``todo`` inherits from @@ -122,81 +107,54 @@ is just a "general" node. `__ and :ref:`Sphinx `. - -The Directive Classes ---------------------- +.. rubric:: The directive classes A directive class is a class deriving usually from -:class:`docutils.parsers.rst.Directive`. The directive interface is also +:class:`docutils.parsers.rst.Directive`. The directive interface is also covered in detail in the `docutils documentation`_; the important thing is that -the class should have attributes that configure the allowed markup, -and a ``run`` method that returns a list of nodes. +the class should have attributes that configure the allowed markup, and a +``run`` method that returns a list of nodes. -The ``todolist`` directive is quite simple:: +Looking first at the ``TodolistDirective`` directive: - from docutils.parsers.rst import Directive +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 24-27 - class TodolistDirective(Directive): +It's very simple, creating and returning an instance of our ``todolist`` node +class. The ``TodolistDirective`` directive itself has neither content nor +arguments that need to be handled. That brings us to the ``TodoDirective`` +directive: - def run(self): - return [todolist('')] +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 30-53 -An instance of our ``todolist`` node class is created and returned. The -todolist directive has neither content nor arguments that need to be handled. - -The ``todo`` directive function looks like this:: - - from sphinx.locale import _ - - class TodoDirective(Directive): - - # this enables content in the directive - has_content = True - - def run(self): - env = self.state.document.settings.env - - targetid = "todo-%d" % env.new_serialno('todo') - targetnode = nodes.target('', '', ids=[targetid]) - - todo_node = todo('\n'.join(self.content)) - todo_node += nodes.title(_('Todo'), _('Todo')) - self.state.nested_parse(self.content, self.content_offset, todo_node) - - if not hasattr(env, 'todo_all_todos'): - env.todo_all_todos = [] - env.todo_all_todos.append({ - 'docname': env.docname, - 'lineno': self.lineno, - 'todo': todo_node.deepcopy(), - 'target': targetnode, - }) - - return [targetnode, todo_node] - -Several important things are covered here. First, as you can see, you can refer -to the :ref:`build environment instance ` using ``self.state.document.settings.env``. - -Then, to act as a link target (from the todolist), the todo directive needs to -return a target node in addition to the todo node. The target ID (in HTML, this -will be the anchor name) is generated by using ``env.new_serialno`` which -returns a new unique integer on each call and therefore leads to unique target -names. The target node is instantiated without any text (the first two -arguments). +Several important things are covered here. First, as you can see, we're now +subclassing the :class:`~sphinx.util.docutils.SphinxDirective` helper class +instead of the usual :class:`~docutils.parsers.rst.Directive` class. This +gives us access to the :ref:`build environment instance ` +using the ``self.env`` property. Without this, we'd have to use the rather +convoluted ``self.state.document.settings.env``. Then, to act as a link target +(from ``TodolistDirective``), the ``TodoDirective`` directive needs to return a +target node in addition to the ``todo`` node. The target ID (in HTML, this will +be the anchor name) is generated by using ``env.new_serialno`` which returns a +new unique integer on each call and therefore leads to unique target names. The +target node is instantiated without any text (the first two arguments). On creating admonition node, the content body of the directive are parsed using ``self.state.nested_parse``. The first argument gives the content body, and the second one gives content offset. The third argument gives the parent node -of parsed result, in our case the ``todo`` node. - -Then, the todo node is added to the environment. This is needed to be able to -create a list of all todo entries throughout the documentation, in the place -where the author puts a ``todolist`` directive. For this case, the environment -attribute ``todo_all_todos`` is used (again, the name should be unique, so it is -prefixed by the extension name). It does not exist when a new environment is -created, so the directive must check and create it if necessary. Various -information about the todo entry's location are stored along with a copy of the -node. +of parsed result, in our case the ``todo`` node. Following this, the ``todo`` +node is added to the environment. This is needed to be able to create a list of +all todo entries throughout the documentation, in the place where the author +puts a ``todolist`` directive. For this case, the environment attribute +``todo_all_todos`` is used (again, the name should be unique, so it is prefixed +by the extension name). It does not exist when a new environment is created, so +the directive must check and create it if necessary. Various information about +the todo entry's location are stored along with a copy of the node. In the last line, the nodes that should be put into the doctree are returned: the target node and the admonition node. @@ -217,18 +175,20 @@ The node structure that the directive returns looks like this:: | ... | +--------------------+ +.. rubric:: The event handlers -The Event Handlers ------------------- +Event handlers are one of Sphinx's most powerful features, providing a way to +do hook into any part of the documentation process. There are many events +provided by Sphinx itself, as detailed in :ref:`the API guide `, and +we're going to use a subset of them here. -Finally, let's look at the event handlers. First, the one for the -:event:`env-purge-doc` event:: +Let's look at the event handlers used in the above example. First, the one for +the :event:`env-purge-doc` event: - def purge_todos(app, env, docname): - if not hasattr(env, 'todo_all_todos'): - return - env.todo_all_todos = [todo for todo in env.todo_all_todos - if todo['docname'] != docname] +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 56-61 Since we store information from source files in the environment, which is persistent, it may become out of date when the source file changes. Therefore, @@ -238,62 +198,144 @@ Here we clear out all todos whose docname matches the given one from the ``todo_all_todos`` list. If there are todos left in the document, they will be added again during parsing. -The other handler belongs to the :event:`doctree-resolved` event. This event is -emitted at the end of :ref:`phase 3 ` and allows custom resolving -to be done:: +The other handler belongs to the :event:`doctree-resolved` event: - def process_todo_nodes(app, doctree, fromdocname): - if not app.config.todo_include_todos: - for node in doctree.traverse(todo): - node.parent.remove(node) +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 64-103 - # Replace all todolist nodes with a list of the collected todos. - # Augment each todo with a backlink to the original location. - env = app.builder.env - - for node in doctree.traverse(todolist): - if not app.config.todo_include_todos: - node.replace_self([]) - continue - - content = [] - - for todo_info in env.todo_all_todos: - para = nodes.paragraph() - filename = env.doc2path(todo_info['docname'], base=None) - description = ( - _('(The original entry is located in %s, line %d and can be found ') % - (filename, todo_info['lineno'])) - para += nodes.Text(description, description) - - # Create a reference - newnode = nodes.reference('', '') - innernode = nodes.emphasis(_('here'), _('here')) - newnode['refdocname'] = todo_info['docname'] - newnode['refuri'] = app.builder.get_relative_uri( - fromdocname, todo_info['docname']) - newnode['refuri'] += '#' + todo_info['target']['refid'] - newnode.append(innernode) - para += newnode - para += nodes.Text('.)', '.)') - - # Insert into the todolist - content.append(todo_info['todo']) - content.append(para) - - node.replace_self(content) - -It is a bit more involved. If our new "todo_include_todos" config value is -false, all todo and todolist nodes are removed from the documents. - -If not, todo nodes just stay where and how they are. Todolist nodes are +The :event:`doctree-resolved` event is emitted at the end of :ref:`phase 3 +(resolving) ` and allows custom resolving to be done. The handler +we have written for this event is a bit more involved. If the +``todo_include_todos`` config value (which we'll describe shortly) is false, +all ``todo`` and ``todolist`` nodes are removed from the documents. If not, +``todo`` nodes just stay where and how they are. ``todolist`` nodes are replaced by a list of todo entries, complete with backlinks to the location -where they come from. The list items are composed of the nodes from the todo -entry and docutils nodes created on the fly: a paragraph for each entry, -containing text that gives the location, and a link (reference node containing -an italic node) with the backreference. The reference URI is built by -``app.builder.get_relative_uri`` which creates a suitable URI depending on the -used builder, and appending the todo node's (the target's) ID as the anchor -name. +where they come from. The list items are composed of the nodes from the +``todo`` entry and docutils nodes created on the fly: a paragraph for each +entry, containing text that gives the location, and a link (reference node +containing an italic node) with the backreference. The reference URI is built +by :meth:`sphinx.builders.Builder.get_relative_uri`` which creates a suitable +URI depending on the used builder, and appending the todo node's (the target's) +ID as the anchor name. +.. rubric:: The ``setup`` function + +.. currentmodule:: sphinx.application + +As noted :doc:`previously `, the ``setup`` function is a requirement +and is used to plug directives into Sphinx. However, we also use it to hook up +the other parts of our extension. Let's look at our ``setup`` function: + +.. literalinclude:: examples/todo.py + :language: python + :linenos: + :lines: 106- + +The calls in this function refer to the classes and functions we added earlier. +What the individual calls do is the following: + +* :meth:`~Sphinx.add_config_value` lets Sphinx know that it should recognize the + new *config value* ``todo_include_todos``, whose default value should be + ``False`` (this also tells Sphinx that it is a boolean value). + + If the third argument was ``'html'``, HTML documents would be full rebuild if the + config value changed its value. This is needed for config values that + influence reading (build :ref:`phase 1 (reading) `). + +* :meth:`~Sphinx.add_node` adds a new *node class* to the build system. It also + can specify visitor functions for each supported output format. These visitor + functions are needed when the new nodes stay until :ref:`phase 4 (writing) + `. Since the ``todolist`` node is always replaced in + :ref:`phase 3 (resolving) `, it doesn't need any. + +* :meth:`~Sphinx.add_directive` adds a new *directive*, given by name and class. + +* Finally, :meth:`~Sphinx.connect` adds an *event handler* to the event whose + name is given by the first argument. The event handler function is called + with several arguments which are documented with the event. + +With this, our extension is complete. + + +Using the extension +------------------- + +As before, we need to enable the extension by declaring it in our +:file:`conf.py` file. There are two steps necessary here: + +#. Add the :file:`_ext` directory to the `Python path`_ using + ``sys.path.append``. This should be placed at the top of the file. + +#. Update or create the :confval:`extensions` list and add the extension file + name to the list + +In addition, we may wish to set the ``todo_include_todos`` config value. As +noted above, this defaults to ``False`` but we can set it explicitly. + +For example: + +.. code-block:: python + + import os + import sys + + sys.path.append(os.path.abspath("./_ext")) + + extensions = ['todo'] + + todo_include_todos = False + +You can now use the extension throughout your project. For example: + +.. code-block:: rst + :caption: index.rst + + Hello, world + ============ + + .. toctree:: + somefile.rst + someotherfile.rst + + Hello world. Below is the list of TODOs. + + .. todolist:: + +.. code-block:: rst + :caption: somefile.rst + + foo + === + + Some intro text here... + + .. todo:: Fix this + +.. code-block:: rst + :caption: someotherfile.rst + + bar + === + + Some more text here... + + .. todo:: Fix that + +Because we have configured ``todo_include_todos`` to ``False``, we won't +actually see anything rendered for the ``todo`` and ``todolist`` directives. +However, if we toggle this to true, we will see the output described +previously. + + +Further reading +--------------- + +For more information, refer to the `docutils`_ documentation and +:doc:`/extdev/index`. + + +.. _docutils: http://docutils.sourceforge.net/docs/ +.. _Python path: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH .. _docutils documentation: http://docutils.sourceforge.net/docs/ref/rst/directives.html diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index fe64628a4..4cb8501be 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -54,8 +54,6 @@ package. .. automethod:: Sphinx.add_domain(domain) -.. automethod:: Sphinx.override_domain(domain) - .. method:: Sphinx.add_directive_to_domain(domain, name, func, content, arguments, \*\*options) .. automethod:: Sphinx.add_directive_to_domain(domain, name, directiveclass) diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst new file mode 100644 index 000000000..99abc56eb --- /dev/null +++ b/doc/extdev/deprecated.rst @@ -0,0 +1,1032 @@ +.. _dev-deprecated-apis: + +Deprecated APIs +=============== + +On developing Sphinx, we are always careful to the compatibility of our APIs. +But, sometimes, the change of interface are needed for some reasons. In such +cases, we've marked them as deprecated. And they are kept during the two +major versions (for more details, please see :ref:`deprecation-policy`). + +The following is a list of deprecated interfaces. + +.. tabularcolumns:: |>{\raggedright}\Y{.4}|>{\centering}\Y{.1}|>{\centering}\Y{.12}|>{\raggedright\arraybackslash}\Y{.38}| + +.. |LaTeXHyphenate| raw:: latex + + \hspace{0pt} + +.. list-table:: deprecated APIs + :header-rows: 1 + :class: deprecated + :widths: 40, 10, 10, 40 + + * - Target + - |LaTeXHyphenate|\ Deprecated + - (will be) Removed + - Alternatives + + * - ``sphinx.builders.latex.LaTeXBuilder.apply_transforms()`` + - 2.1 + - 4.0 + - N/A + + * - ``sphinx.directives.Acks`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.Acks`` + + * - ``sphinx.directives.Author`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.Author`` + + * - ``sphinx.directives.Centered`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.Centered`` + + * - ``sphinx.directives.Class`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.Class`` + + * - ``sphinx.directives.CodeBlock`` + - 2.1 + - 4.0 + - ``sphinx.directives.code.CodeBlock`` + + * - ``sphinx.directives.Figure`` + - 2.1 + - 4.0 + - ``sphinx.directives.patches.Figure`` + + * - ``sphinx.directives.HList`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.HList`` + + * - ``sphinx.directives.Highlight`` + - 2.1 + - 4.0 + - ``sphinx.directives.code.Highlight`` + + * - ``sphinx.directives.Include`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.Include`` + + * - ``sphinx.directives.Index`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.Index`` + + * - ``sphinx.directives.LiteralInclude`` + - 2.1 + - 4.0 + - ``sphinx.directives.code.LiteralInclude`` + + * - ``sphinx.directives.Meta`` + - 2.1 + - 4.0 + - ``sphinx.directives.patches.Meta`` + + * - ``sphinx.directives.Only`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.Only`` + + * - ``sphinx.directives.SeeAlso`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.SeeAlso`` + + * - ``sphinx.directives.TabularColumns`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.TabularColumns`` + + * - ``sphinx.directives.TocTree`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.TocTree`` + + * - ``sphinx.directives.VersionChange`` + - 2.1 + - 4.0 + - ``sphinx.directives.other.VersionChange`` + + * - ``sphinx.environment.NoUri`` + - 2.1 + - 4.0 + - ``sphinx.errors.NoUri`` + + * - ``sphinx.ext.autodoc.importer.MockFinder`` + - 2.1 + - 4.0 + - ``sphinx.ext.autodoc.mock.MockFinder`` + + * - ``sphinx.ext.autodoc.importer.MockLoader`` + - 2.1 + - 4.0 + - ``sphinx.ext.autodoc.mock.MockLoader`` + + * - ``sphinx.ext.autodoc.importer.mock()`` + - 2.1 + - 4.0 + - ``sphinx.ext.autodoc.mock.mock()`` + + * - ``sphinx.ext.autosummary.autolink_role()`` + - 2.1 + - 4.0 + - ``sphinx.ext.autosummary.AutoLink`` + + * - ``sphinx.util.docfields.DocFieldTransformer.preprocess_fieldtypes()`` + - 2.1 + - 4.0 + - ``sphinx.directives.ObjectDescription.get_field_type_map()`` + + * - ``sphinx.util.node.find_source_node()`` + - 2.1 + - 4.0 + - ``sphinx.util.node.get_node_source()`` + + * - ``sphinx.util.i18n.find_catalog()`` + - 2.1 + - 4.0 + - ``sphinx.util.i18n.docname_to_domain()`` + + * - ``sphinx.util.i18n.find_catalog_files()`` + - 2.1 + - 4.0 + - ``sphinx.util.i18n.CatalogRepository`` + + * - ``sphinx.util.i18n.find_catalog_source_files()`` + - 2.1 + - 4.0 + - ``sphinx.util.i18n.CatalogRepository`` + + * - ``encoding`` argument of ``autodoc.Documenter.get_doc()``, + ``autodoc.DocstringSignatureMixin.get_doc()``, + ``autodoc.DocstringSignatureMixin._find_signature()``, and + ``autodoc.ClassDocumenter.get_doc()`` + - 2.0 + - 4.0 + - N/A + + * - arguments of ``EpubBuilder.build_mimetype()``, + ``EpubBuilder.build_container()``, ``EpubBuilder.build_content()``, + ``EpubBuilder.build_toc()`` and ``EpubBuilder.build_epub()`` + - 2.0 + - 4.0 + - N/A + + * - arguments of ``Epub3Builder.build_navigation_doc()`` + - 2.0 + - 4.0 + - N/A + + * - ``nodetype`` argument of + ``sphinx.search.WordCollector.is_meta_keywords()`` + - 2.0 + - 4.0 + - N/A + + * - ``suffix`` argument of ``BuildEnvironment.doc2path()`` + - 2.0 + - 4.0 + - N/A + + * - string style ``base`` argument of ``BuildEnvironment.doc2path()`` + - 2.0 + - 4.0 + - ``os.path.join()`` + + * - ``sphinx.addnodes.abbreviation`` + - 2.0 + - 4.0 + - ``docutils.nodes.abbreviation`` + + * - ``sphinx.builders.applehelp`` + - 2.0 + - 4.0 + - ``sphinxcontrib.applehelp`` + + * - ``sphinx.builders.devhelp`` + - 2.0 + - 4.0 + - ``sphinxcontrib.devhelp`` + + * - ``sphinx.builders.epub3.Epub3Builder.validate_config_value()`` + - 2.0 + - 4.0 + - ``sphinx.builders.epub3.validate_config_values()`` + + * - ``sphinx.builders.html.JSONHTMLBuilder`` + - 2.0 + - 4.0 + - ``sphinx.builders.serializinghtml.JSONHTMLBuilder`` + + * - ``sphinx.builders.html.PickleHTMLBuilder`` + - 2.0 + - 4.0 + - ``sphinx.builders.serializinghtml.PickleHTMLBuilder`` + + * - ``sphinx.builders.html.SerializingHTMLBuilder`` + - 2.0 + - 4.0 + - ``sphinx.builders.serializinghtml.SerializingHTMLBuilder`` + + * - ``sphinx.builders.html.SingleFileHTMLBuilder`` + - 2.0 + - 4.0 + - ``sphinx.builders.singlehtml.SingleFileHTMLBuilder`` + + * - ``sphinx.builders.html.WebHTMLBuilder`` + - 2.0 + - 4.0 + - ``sphinx.builders.serializinghtml.PickleHTMLBuilder`` + + * - ``sphinx.builders.htmlhelp`` + - 2.0 + - 4.0 + - ``sphinxcontrib.htmlhelp`` + + * - ``sphinx.builders.htmlhelp.HTMLHelpBuilder.open_file()`` + - 2.0 + - 4.0 + - ``open()`` + + * - ``sphinx.builders.qthelp`` + - 2.0 + - 4.0 + - ``sphinxcontrib.qthelp`` + + * - ``sphinx.cmd.quickstart.term_decode()`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.cmd.quickstart.TERM_ENCODING`` + - 2.0 + - 4.0 + - ``sys.stdin.encoding`` + + * - ``sphinx.config.check_unicode()`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.config.string_classes`` + - 2.0 + - 4.0 + - ``[str]`` + + * - ``sphinx.domains.cpp.DefinitionError.description`` + - 2.0 + - 4.0 + - ``str(exc)`` + + * - ``sphinx.domains.cpp.NoOldIdError.description`` + - 2.0 + - 4.0 + - ``str(exc)`` + + * - ``sphinx.domains.cpp.UnsupportedMultiCharacterCharLiteral.decoded`` + - 2.0 + - 4.0 + - ``str(exc)`` + + * - ``sphinx.ext.autosummary.Autosummary.warn()`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.ext.autosummary.Autosummary.genopt`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.ext.autosummary.Autosummary.warnings`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.ext.autosummary.Autosummary.result`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.ext.doctest.doctest_encode()`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.ext.jsmath`` + - 2.0 + - 4.0 + - ``sphinxcontrib.jsmath`` + + * - ``sphinx.roles.abbr_role()`` + - 2.0 + - 4.0 + - ``sphinx.roles.Abbreviation`` + + * - ``sphinx.roles.emph_literal_role()`` + - 2.0 + - 4.0 + - ``sphinx.roles.EmphasizedLiteral`` + + * - ``sphinx.roles.menusel_role()`` + - 2.0 + - 4.0 + - ``sphinx.roles.GUILabel`` or ``sphinx.roles.MenuSelection`` + + * - ``sphinx.roles.index_role()`` + - 2.0 + - 4.0 + - ``sphinx.roles.Index`` + + * - ``sphinx.roles.indexmarkup_role()`` + - 2.0 + - 4.0 + - ``sphinx.roles.PEP`` or ``sphinx.roles.RFC`` + + * - ``sphinx.testing.util.remove_unicode_literal()`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.util.attrdict`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.util.force_decode()`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.util.get_matching_docs()`` + - 2.0 + - 4.0 + - ``sphinx.util.get_matching_files()`` + + * - ``sphinx.util.inspect.Parameter`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.util.jsonimpl`` + - 2.0 + - 4.0 + - ``sphinxcontrib.serializinghtml.jsonimpl`` + + * - ``sphinx.util.osutil.EEXIST`` + - 2.0 + - 4.0 + - ``errno.EEXIST`` or ``FileExistsError`` + + * - ``sphinx.util.osutil.EINVAL`` + - 2.0 + - 4.0 + - ``errno.EINVAL`` + + * - ``sphinx.util.osutil.ENOENT`` + - 2.0 + - 4.0 + - ``errno.ENOENT`` or ``FileNotFoundError`` + + * - ``sphinx.util.osutil.EPIPE`` + - 2.0 + - 4.0 + - ``errno.ENOENT`` or ``BrokenPipeError`` + + * - ``sphinx.util.osutil.walk()`` + - 2.0 + - 4.0 + - ``os.walk()`` + + * - ``sphinx.util.pycompat.NoneType`` + - 2.0 + - 4.0 + - ``sphinx.util.typing.NoneType`` + + * - ``sphinx.util.pycompat.TextIOWrapper`` + - 2.0 + - 4.0 + - ``io.TextIOWrapper`` + + * - ``sphinx.util.pycompat.UnicodeMixin`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.util.pycompat.htmlescape()`` + - 2.0 + - 4.0 + - ``html.escape()`` + + * - ``sphinx.util.pycompat.indent()`` + - 2.0 + - 4.0 + - ``textwrap.indent()`` + + * - ``sphinx.util.pycompat.sys_encoding`` + - 2.0 + - 4.0 + - ``sys.getdefaultencoding()`` + + * - ``sphinx.util.pycompat.terminal_safe()`` + - 2.0 + - 4.0 + - ``sphinx.util.console.terminal_safe()`` + + * - ``sphinx.util.pycompat.u`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.util.PeekableIterator`` + - 2.0 + - 4.0 + - N/A + + * - Omitting the ``filename`` argument in an overriddent + ``IndexBuilder.feed()`` method. + - 2.0 + - 4.0 + - ``IndexBuilder.feed(docname, filename, title, doctree)`` + + * - ``sphinx.writers.latex.ExtBabel`` + - 2.0 + - 4.0 + - ``sphinx.builders.latex.util.ExtBabel`` + + * - ``sphinx.writers.latex.LaTeXTranslator.babel_defmacro()`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.application.Sphinx._setting_up_extension`` + - 2.0 + - 3.0 + - N/A + + * - The ``importer`` argument of ``sphinx.ext.autodoc.importer._MockModule`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.ext.autodoc.importer._MockImporter`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.io.SphinxBaseFileInput`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.io.SphinxFileInput.supported`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.io.SphinxRSTFileInput`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.registry.SphinxComponentRegistry.add_source_input()`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator._make_visit_admonition()`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.collect_footnotes()`` + - 2.0 + - 4.0 + - N/A + + * - ``sphinx.writers.texinfo.TexinfoTranslator._make_visit_admonition()`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.writers.text.TextTranslator._make_depart_admonition()`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.generate_numfig_format()`` + - 2.0 + - 4.0 + - N/A + + * - :rst:dir:`highlightlang` + - 1.8 + - 4.0 + - :rst:dir:`highlight` + + * - :meth:`~sphinx.application.Sphinx.add_stylesheet()` + - 1.8 + - 4.0 + - :meth:`~sphinx.application.Sphinx.add_css_file()` + + * - :meth:`~sphinx.application.Sphinx.add_javascript()` + - 1.8 + - 4.0 + - :meth:`~sphinx.application.Sphinx.add_js_file()` + + * - :confval:`autodoc_default_flags` + - 1.8 + - 4.0 + - :confval:`autodoc_default_options` + + * - ``content`` arguments of ``sphinx.util.image.guess_mimetype()`` + - 1.8 + - 3.0 + - N/A + + * - ``gettext_compact`` arguments of + ``sphinx.util.i18n.find_catalog_source_files()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.io.SphinxI18nReader.set_lineno_for_reporter()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.io.SphinxI18nReader.line`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.directives.other.VersionChanges`` + - 1.8 + - 3.0 + - ``sphinx.domains.changeset.VersionChanges`` + + * - ``sphinx.highlighting.PygmentsBridge.unhighlight()`` + - 1.8 + - 3.0 + - N/A + + * - ``trim_doctest_flags`` arguments of + ``sphinx.highlighting.PygmentsBridge`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.ext.mathbase`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.ext.mathbase.MathDomain`` + - 1.8 + - 3.0 + - ``sphinx.domains.math.MathDomain`` + + * - ``sphinx.ext.mathbase.MathDirective`` + - 1.8 + - 3.0 + - ``sphinx.directives.patches.MathDirective`` + + * - ``sphinx.ext.mathbase.math_role()`` + - 1.8 + - 3.0 + - ``docutils.parsers.rst.roles.math_role()`` + + * - ``sphinx.ext.mathbase.setup_math()`` + - 1.8 + - 3.0 + - :meth:`~sphinx.application.Sphinx.add_html_math_renderer()` + + * - ``sphinx.ext.mathbase.is_in_section_title()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.ext.mathbase.get_node_equation_number()`` + - 1.8 + - 3.0 + - ``sphinx.util.math.get_node_equation_number()`` + + * - ``sphinx.ext.mathbase.wrap_displaymath()`` + - 1.8 + - 3.0 + - ``sphinx.util.math.wrap_displaymath()`` + + * - ``sphinx.ext.mathbase.math`` (node) + - 1.8 + - 3.0 + - ``docutils.nodes.math`` + + * - ``sphinx.ext.mathbase.displaymath`` (node) + - 1.8 + - 3.0 + - ``docutils.nodes.math_block`` + + * - ``sphinx.ext.mathbase.eqref`` (node) + - 1.8 + - 3.0 + - ``sphinx.builders.latex.nodes.math_reference`` + + * - ``viewcode_import`` (config value) + - 1.8 + - 3.0 + - :confval:`viewcode_follow_imported_members` + + * - ``sphinx.writers.latex.Table.caption_footnotetexts`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.Table.header_footnotetexts`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.footnotestack`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.in_container_literal_block`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.next_section_ids`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.next_hyperlink_ids`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.restrict_footnote()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.unrestrict_footnote()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.push_hyperlink_ids()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.pop_hyperlink_ids()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.bibitems`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.hlsettingstack`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.ExtBabel.get_shorthandoff()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.html.HTMLTranslator.highlightlang()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.html.HTMLTranslator.highlightlang_base()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.html.HTMLTranslator.highlightlangopts()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.html.HTMLTranslator.highlightlinenothreshold()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.html5.HTMLTranslator.highlightlang()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.html5.HTMLTranslator.highlightlang_base()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.html5.HTMLTranslator.highlightlangopts()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.html5.HTMLTranslator.highlightlinenothreshold()`` + - 1.8 + - 3.0 + - N/A + + * - ``sphinx.writers.latex.LaTeXTranslator.check_latex_elements()`` + - 1.8 + - 3.0 + - Nothing + + * - ``sphinx.application.CONFIG_FILENAME`` + - 1.8 + - 3.0 + - ``sphinx.config.CONFIG_FILENAME`` + + * - ``Config.check_unicode()`` + - 1.8 + - 3.0 + - ``sphinx.config.check_unicode()`` + + * - ``Config.check_types()`` + - 1.8 + - 3.0 + - ``sphinx.config.check_confval_types()`` + + * - ``dirname``, ``filename`` and ``tags`` arguments of + ``Config.__init__()`` + - 1.8 + - 3.0 + - ``Config.read()`` + + * - The value of :confval:`html_search_options` + - 1.8 + - 3.0 + - see :confval:`html_search_options` + + * - ``sphinx.versioning.prepare()`` + - 1.8 + - 3.0 + - ``sphinx.versioning.UIDTransform`` + + * - ``Sphinx.override_domain()`` + - 1.8 + - 3.0 + - :meth:`~sphinx.application.Sphinx.add_domain()` + + * - ``Sphinx.import_object()`` + - 1.8 + - 3.0 + - ``sphinx.util.import_object()`` + + * - ``suffix`` argument of + :meth:`~sphinx.application.Sphinx.add_source_parser()` + - 1.8 + - 3.0 + - :meth:`~sphinx.application.Sphinx.add_source_suffix()` + + + * - ``BuildEnvironment.load()`` + - 1.8 + - 3.0 + - ``pickle.load()`` + + * - ``BuildEnvironment.loads()`` + - 1.8 + - 3.0 + - ``pickle.loads()`` + + * - ``BuildEnvironment.frompickle()`` + - 1.8 + - 3.0 + - ``pickle.load()`` + + * - ``BuildEnvironment.dump()`` + - 1.8 + - 3.0 + - ``pickle.dump()`` + + * - ``BuildEnvironment.dumps()`` + - 1.8 + - 3.0 + - ``pickle.dumps()`` + + * - ``BuildEnvironment.topickle()`` + - 1.8 + - 3.0 + - ``pickle.dump()`` + + * - ``BuildEnvironment._nitpick_ignore`` + - 1.8 + - 3.0 + - :confval:`nitpick_ignore` + + * - ``BuildEnvironment.versionchanges`` + - 1.8 + - 3.0 + - N/A + + * - ``BuildEnvironment.update()`` + - 1.8 + - 3.0 + - ``Builder.read()`` + + * - ``BuildEnvironment.read_doc()`` + - 1.8 + - 3.0 + - ``Builder.read_doc()`` + + * - ``BuildEnvironment._read_serial()`` + - 1.8 + - 3.0 + - ``Builder.read()`` + + * - ``BuildEnvironment._read_parallel()`` + - 1.8 + - 3.0 + - ``Builder.read()`` + + * - ``BuildEnvironment.write_doctree()`` + - 1.8 + - 3.0 + - ``Builder.write_doctree()`` + + * - ``BuildEnvironment.note_versionchange()`` + - 1.8 + - 3.0 + - ``ChangesDomain.note_changeset()`` + + * - ``warn()`` (template helper function) + - 1.8 + - 3.0 + - ``warning()`` + + * - :confval:`source_parsers` + - 1.8 + - 3.0 + - :meth:`~sphinx.application.Sphinx.add_source_parser()` + + * - ``sphinx.util.docutils.directive_helper()`` + - 1.8 + - 3.0 + - ``Directive`` class of docutils + + * - ``sphinx.cmdline`` + - 1.8 + - 3.0 + - ``sphinx.cmd.build`` + + * - ``sphinx.make_mode`` + - 1.8 + - 3.0 + - ``sphinx.cmd.make_mode`` + + * - ``sphinx.locale.l_()`` + - 1.8 + - 3.0 + - :func:`sphinx.locale._()` + + * - ``sphinx.locale.lazy_gettext()`` + - 1.8 + - 3.0 + - :func:`sphinx.locale._()` + + * - ``sphinx.locale.mygettext()`` + - 1.8 + - 3.0 + - :func:`sphinx.locale._()` + + * - ``sphinx.util.copy_static_entry()`` + - 1.5 + - 3.0 + - ``sphinx.util.fileutil.copy_asset()`` + + * - ``sphinx.build_main()`` + - 1.7 + - 2.0 + - ``sphinx.cmd.build.build_main()`` + + * - ``sphinx.ext.intersphinx.debug()`` + - 1.7 + - 2.0 + - ``sphinx.ext.intersphinx.inspect_main()`` + + * - ``sphinx.ext.autodoc.format_annotation()`` + - 1.7 + - 2.0 + - ``sphinx.util.inspect.Signature`` + + * - ``sphinx.ext.autodoc.formatargspec()`` + - 1.7 + - 2.0 + - ``sphinx.util.inspect.Signature`` + + * - ``sphinx.ext.autodoc.AutodocReporter`` + - 1.7 + - 2.0 + - ``sphinx.util.docutils.switch_source_input()`` + + * - ``sphinx.ext.autodoc.add_documenter()`` + - 1.7 + - 2.0 + - :meth:`~sphinx.application.Sphinx.add_autodocumenter()` + + * - ``sphinx.ext.autodoc.AutoDirective._register`` + - 1.7 + - 2.0 + - :meth:`~sphinx.application.Sphinx.add_autodocumenter()` + + * - ``AutoDirective._special_attrgetters`` + - 1.7 + - 2.0 + - :meth:`~sphinx.application.Sphinx.add_autodoc_attrgetter()` + + * - ``Sphinx.warn()``, ``Sphinx.info()`` + - 1.6 + - 2.0 + - :ref:`logging-api` + + * - ``BuildEnvironment.set_warnfunc()`` + - 1.6 + - 2.0 + - :ref:`logging-api` + + * - ``BuildEnvironment.note_toctree()`` + - 1.6 + - 2.0 + - ``Toctree.note()`` (in ``sphinx.environment.adapters.toctree``) + + * - ``BuildEnvironment.get_toc_for()`` + - 1.6 + - 2.0 + - ``Toctree.get_toc_for()`` (in ``sphinx.environment.adapters.toctree``) + + * - ``BuildEnvironment.get_toctree_for()`` + - 1.6 + - 2.0 + - ``Toctree.get_toctree_for()`` (in ``sphinx.environment.adapters.toctree``) + + * - ``BuildEnvironment.create_index()`` + - 1.6 + - 2.0 + - ``IndexEntries.create_index()`` (in ``sphinx.environment.adapters.indexentries``) + + * - ``sphinx.websupport`` + - 1.6 + - 2.0 + - `sphinxcontrib-websupport `_ + + * - ``StandaloneHTMLBuilder.css_files`` + - 1.6 + - 2.0 + - :meth:`~sphinx.application.Sphinx.add_stylesheet()` + + * - ``document.settings.gettext_compact`` + - 1.8 + - 1.8 + - :confval:`gettext_compact` + + * - ``Sphinx.status_iterator()`` + - 1.6 + - 1.7 + - ``sphinx.util.status_iterator()`` + + * - ``Sphinx.old_status_iterator()`` + - 1.6 + - 1.7 + - ``sphinx.util.old_status_iterator()`` + + * - ``Sphinx._directive_helper()`` + - 1.6 + - 1.7 + - ``sphinx.util.docutils.directive_helper()`` + + * - ``sphinx.util.compat.Directive`` + - 1.6 + - 1.7 + - ``docutils.parsers.rst.Directive`` + + * - ``sphinx.util.compat.docutils_version`` + - 1.6 + - 1.7 + - ``sphinx.util.docutils.__version_info__`` + +.. note:: On deprecating on public APIs (internal functions and classes), + we also follow the policy as much as possible. diff --git a/doc/extdev/i18n.rst b/doc/extdev/i18n.rst index c8c54da36..c3ec173a2 100644 --- a/doc/extdev/i18n.rst +++ b/doc/extdev/i18n.rst @@ -15,3 +15,83 @@ i18n API .. autofunction:: __ + +.. _ext-i18n: + +Extension internationalization (`i18n`) and localization (`l10n`) using i18n API +--------------------------------------------------------------------------------- + +.. versionadded:: 1.8 + +An extension may naturally come with message translations. This is briefly +summarized in :func:`sphinx.locale.get_translation` help. + +In practice, you have to: + +#. Choose a name for your message catalog, which must be unique. Usually + the name of your extension is used for the name of message catalog. + +#. Mark in your extension sources all messages as translatable, via + :func:`sphinx.locale.get_translation` function, usually renamed ``_()``, + e.g.: + + .. code-block:: python + :caption: src/__init__.py + + from sphinx.locale import get_translation + + MESSAGE_CATALOG_NAME = 'myextension' + _ = get_translation(MESSAGE_CATALOG_NAME) + + translated_text = _('Hello Sphinx!') + +#. Set up your extension to be aware of its dedicated translations: + + .. code-block:: python + :caption: src/__init__.py + + def setup(app): + package_dir = path.abspath(path.dirname(__file__)) + locale_dir = os.path.join(package_dir, 'locales') + app.add_message_catalog(MESSAGE_CATALOG_NAME, locale_dir) + +#. Generate message catalog template ``*.pot`` file, usually in ``locale/`` + source directory, for example via `Babel`_: + + .. code-block:: console + + $ pybabel extract --output=src/locale/myextension.pot src/ + +#. Create message catalogs (``*.po``) for each language which your extension + will provide localization, for example via `Babel`_: + + .. code-block:: console + + $ pybabel init --input-file=src/locale/myextension.pot --domain=myextension --output-dir=src/locale --locale=fr_FR + +#. Translate message catalogs for each language manually + +#. Compile message catalogs into ``*.mo`` files, for example via `Babel`_: + + .. code-block:: console + + $ pybabel compile --directory=src/locale --domain=myextension + +#. Ensure that message catalog files are distributed when your package will + be installed, by adding equivalent line in your extension ``MANIFEST.in``: + + .. code-block:: ini + :caption: MANIFEST.in + + recursive-include src *.pot *.po *.mo + + +When the messages on your extension has been changed, you need to also update +message catalog template and message catalogs, for example via `Babel`_: + +.. code-block:: console + + $ pybabel extract --output=src/locale/myextension.pot src/ + $ pybabel update --input-file=src/locale/myextension.pot --domain=myextension --output-dir=src/locale + +.. _Babel: http://babel.pocoo.org/ diff --git a/doc/extdev/index.rst b/doc/extdev/index.rst index 7b4bbc692..eac4ded40 100644 --- a/doc/extdev/index.rst +++ b/doc/extdev/index.rst @@ -187,6 +187,7 @@ as metadata of the extension. Metadata keys currently recognized are: output files can be used when the extension is loaded. Since extensions usually don't negatively influence the process, this defaults to ``True``. + APIs used for writing extensions -------------------------------- @@ -205,896 +206,4 @@ APIs used for writing extensions logging i18n utils - -.. _dev-deprecated-apis: - -Deprecated APIs ---------------- - -On developing Sphinx, we are always careful to the compatibility of our APIs. -But, sometimes, the change of interface are needed for some reasons. In such -cases, we've marked them as deprecated. And they are kept during the two -major versions (for more details, please see :ref:`deprecation-policy`). - -The following is a list of deprecated interfaces. - -.. tabularcolumns:: |>{\raggedright}\Y{.4}|>{\centering}\Y{.1}|>{\centering}\Y{.12}|>{\raggedright\arraybackslash}\Y{.38}| - -.. |LaTeXHyphenate| raw:: latex - - \hspace{0pt} - -.. list-table:: deprecated APIs - :header-rows: 1 - :class: deprecated - :widths: 40, 10, 10, 40 - - * - Target - - |LaTeXHyphenate|\ Deprecated - - (will be) Removed - - Alternatives - - * - ``encoding`` argument of ``autodoc.Documenter.get_doc()``, - ``autodoc.DocstringSignatureMixin.get_doc()``, - ``autodoc.DocstringSignatureMixin._find_signature()``, and - ``autodoc.ClassDocumenter.get_doc()`` - - 2.0 - - 4.0 - - N/A - - * - arguments of ``EpubBuilder.build_mimetype()``, - ``EpubBuilder.build_container()``, ``EpubBuilder.build_content()``, - ``EpubBuilder.build_toc()`` and ``EpubBuilder.build_epub()`` - - 2.0 - - 4.0 - - N/A - - * - arguments of ``Epub3Builder.build_navigation_doc()`` - - 2.0 - - 4.0 - - N/A - - * - ``nodetype`` argument of - ``sphinx.search.WordCollector.is_meta_keywords()`` - - 2.0 - - 4.0 - - N/A - - * - ``suffix`` argument of ``BuildEnvironment.doc2path()`` - - 2.0 - - 4.0 - - N/A - - * - string style ``base`` argument of ``BuildEnvironment.doc2path()`` - - 2.0 - - 4.0 - - ``os.path.join()`` - - * - ``sphinx.addnodes.abbreviation`` - - 2.0 - - 4.0 - - ``docutils.nodes.abbreviation`` - - * - ``sphinx.builders.applehelp`` - - 2.0 - - 4.0 - - ``sphinxcontrib.applehelp`` - - * - ``sphinx.builders.devhelp`` - - 2.0 - - 4.0 - - ``sphinxcontrib.devhelp`` - - * - ``sphinx.builders.epub3.Epub3Builder.validate_config_value()`` - - 2.0 - - 4.0 - - ``sphinx.builders.epub3.validate_config_values()`` - - * - ``sphinx.builders.html.JSONHTMLBuilder`` - - 2.0 - - 4.0 - - ``sphinx.builders.serializinghtml.JSONHTMLBuilder`` - - * - ``sphinx.builders.html.PickleHTMLBuilder`` - - 2.0 - - 4.0 - - ``sphinx.builders.serializinghtml.PickleHTMLBuilder`` - - * - ``sphinx.builders.html.SerializingHTMLBuilder`` - - 2.0 - - 4.0 - - ``sphinx.builders.serializinghtml.SerializingHTMLBuilder`` - - * - ``sphinx.builders.html.SingleFileHTMLBuilder`` - - 2.0 - - 4.0 - - ``sphinx.builders.singlehtml.SingleFileHTMLBuilder`` - - * - ``sphinx.builders.html.WebHTMLBuilder`` - - 2.0 - - 4.0 - - ``sphinx.builders.serializinghtml.PickleHTMLBuilder`` - - * - ``sphinx.builders.htmlhelp`` - - 2.0 - - 4.0 - - ``sphinxcontrib.htmlhelp`` - - * - ``sphinx.builders.htmlhelp.HTMLHelpBuilder.open_file()`` - - 2.0 - - 4.0 - - ``open()`` - - * - ``sphinx.builders.qthelp`` - - 2.0 - - 4.0 - - ``sphinxcontrib.qthelp`` - - * - ``sphinx.cmd.quickstart.term_decode()`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.cmd.quickstart.TERM_ENCODING`` - - 2.0 - - 4.0 - - ``sys.stdin.encoding`` - - * - ``sphinx.config.check_unicode()`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.config.string_classes`` - - 2.0 - - 4.0 - - ``[str]`` - - * - ``sphinx.domains.cpp.DefinitionError.description`` - - 2.0 - - 4.0 - - ``str(exc)`` - - * - ``sphinx.domains.cpp.NoOldIdError.description`` - - 2.0 - - 4.0 - - ``str(exc)`` - - * - ``sphinx.domains.cpp.UnsupportedMultiCharacterCharLiteral.decoded`` - - 2.0 - - 4.0 - - ``str(exc)`` - - * - ``sphinx.ext.autosummary.Autosummary.warn()`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.ext.autosummary.Autosummary.genopt`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.ext.autosummary.Autosummary.warnings`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.ext.autosummary.Autosummary.result`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.ext.doctest.doctest_encode()`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.ext.jsmath`` - - 2.0 - - 4.0 - - ``sphinxcontrib.jsmath`` - - * - ``sphinx.roles.abbr_role()`` - - 2.0 - - 4.0 - - ``sphinx.roles.Abbreviation`` - - * - ``sphinx.roles.emph_literal_role()`` - - 2.0 - - 4.0 - - ``sphinx.roles.EmphasizedLiteral`` - - * - ``sphinx.roles.menusel_role()`` - - 2.0 - - 4.0 - - ``sphinx.roles.GUILabel`` or ``sphinx.roles.MenuSelection`` - - * - ``sphinx.roles.index_role()`` - - 2.0 - - 4.0 - - ``sphinx.roles.Index`` - - * - ``sphinx.roles.indexmarkup_role()`` - - 2.0 - - 4.0 - - ``sphinx.roles.PEP`` or ``sphinx.roles.RFC`` - - * - ``sphinx.testing.util.remove_unicode_literal()`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.util.attrdict`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.util.force_decode()`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.util.get_matching_docs()`` - - 2.0 - - 4.0 - - ``sphinx.util.get_matching_files()`` - - * - ``sphinx.util.inspect.Parameter`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.util.jsonimpl`` - - 2.0 - - 4.0 - - ``sphinxcontrib.serializinghtml.jsonimpl`` - - * - ``sphinx.util.osutil.EEXIST`` - - 2.0 - - 4.0 - - ``errno.EEXIST`` or ``FileExistsError`` - - * - ``sphinx.util.osutil.EINVAL`` - - 2.0 - - 4.0 - - ``errno.EINVAL`` - - * - ``sphinx.util.osutil.ENOENT`` - - 2.0 - - 4.0 - - ``errno.ENOENT`` or ``FileNotFoundError`` - - * - ``sphinx.util.osutil.EPIPE`` - - 2.0 - - 4.0 - - ``errno.ENOENT`` or ``BrokenPipeError`` - - * - ``sphinx.util.osutil.walk()`` - - 2.0 - - 4.0 - - ``os.walk()`` - - * - ``sphinx.util.pycompat.NoneType`` - - 2.0 - - 4.0 - - ``sphinx.util.typing.NoneType`` - - * - ``sphinx.util.pycompat.TextIOWrapper`` - - 2.0 - - 4.0 - - ``io.TextIOWrapper`` - - * - ``sphinx.util.pycompat.UnicodeMixin`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.util.pycompat.htmlescape()`` - - 2.0 - - 4.0 - - ``html.escape()`` - - * - ``sphinx.util.pycompat.indent()`` - - 2.0 - - 4.0 - - ``textwrap.indent()`` - - * - ``sphinx.util.pycompat.sys_encoding`` - - 2.0 - - 4.0 - - ``sys.getdefaultencoding()`` - - * - ``sphinx.util.pycompat.terminal_safe()`` - - 2.0 - - 4.0 - - ``sphinx.util.console.terminal_safe()`` - - * - ``sphinx.util.pycompat.u`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.util.PeekableIterator`` - - 2.0 - - 4.0 - - N/A - - * - Omitting the ``filename`` argument in an overriddent - ``IndexBuilder.feed()`` method. - - 2.0 - - 4.0 - - ``IndexBuilder.feed(docname, filename, title, doctree)`` - - * - ``sphinx.writers.latex.ExtBabel`` - - 2.0 - - 4.0 - - ``sphinx.builders.latex.util.ExtBabel`` - - * - ``sphinx.writers.latex.LaTeXTranslator.babel_defmacro()`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.application.Sphinx._setting_up_extension`` - - 2.0 - - 3.0 - - N/A - - * - The ``importer`` argument of ``sphinx.ext.autodoc.importer._MockModule`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.ext.autodoc.importer._MockImporter`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.io.SphinxBaseFileInput`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.io.SphinxFileInput.supported`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.io.SphinxRSTFileInput`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.registry.SphinxComponentRegistry.add_source_input()`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator._make_visit_admonition()`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.collect_footnotes()`` - - 2.0 - - 4.0 - - N/A - - * - ``sphinx.writers.texinfo.TexinfoTranslator._make_visit_admonition()`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.writers.text.TextTranslator._make_depart_admonition()`` - - 2.0 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.generate_numfig_format()`` - - 2.0 - - 4.0 - - N/A - - * - :rst:dir:`highlightlang` - - 1.8 - - 4.0 - - :rst:dir:`highlight` - - * - :meth:`~sphinx.application.Sphinx.add_stylesheet()` - - 1.8 - - 4.0 - - :meth:`~sphinx.application.Sphinx.add_css_file()` - - * - :meth:`~sphinx.application.Sphinx.add_javascript()` - - 1.8 - - 4.0 - - :meth:`~sphinx.application.Sphinx.add_js_file()` - - * - :confval:`autodoc_default_flags` - - 1.8 - - 4.0 - - :confval:`autodoc_default_options` - - * - ``content`` arguments of ``sphinx.util.image.guess_mimetype()`` - - 1.8 - - 3.0 - - N/A - - * - ``gettext_compact`` arguments of - ``sphinx.util.i18n.find_catalog_source_files()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.io.SphinxI18nReader.set_lineno_for_reporter()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.io.SphinxI18nReader.line`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.directives.other.VersionChanges`` - - 1.8 - - 3.0 - - ``sphinx.domains.changeset.VersionChanges`` - - * - ``sphinx.highlighting.PygmentsBridge.unhighlight()`` - - 1.8 - - 3.0 - - N/A - - * - ``trim_doctest_flags`` arguments of - ``sphinx.highlighting.PygmentsBridge`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.ext.mathbase`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.ext.mathbase.MathDomain`` - - 1.8 - - 3.0 - - ``sphinx.domains.math.MathDomain`` - - * - ``sphinx.ext.mathbase.MathDirective`` - - 1.8 - - 3.0 - - ``sphinx.directives.patches.MathDirective`` - - * - ``sphinx.ext.mathbase.math_role()`` - - 1.8 - - 3.0 - - ``docutils.parsers.rst.roles.math_role()`` - - * - ``sphinx.ext.mathbase.setup_math()`` - - 1.8 - - 3.0 - - :meth:`~sphinx.application.Sphinx.add_html_math_renderer()` - - * - ``sphinx.ext.mathbase.is_in_section_title()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.ext.mathbase.get_node_equation_number()`` - - 1.8 - - 3.0 - - ``sphinx.util.math.get_node_equation_number()`` - - * - ``sphinx.ext.mathbase.wrap_displaymath()`` - - 1.8 - - 3.0 - - ``sphinx.util.math.wrap_displaymath()`` - - * - ``sphinx.ext.mathbase.math`` (node) - - 1.8 - - 3.0 - - ``docutils.nodes.math`` - - * - ``sphinx.ext.mathbase.displaymath`` (node) - - 1.8 - - 3.0 - - ``docutils.nodes.math_block`` - - * - ``sphinx.ext.mathbase.eqref`` (node) - - 1.8 - - 3.0 - - ``sphinx.builders.latex.nodes.math_reference`` - - * - ``viewcode_import`` (config value) - - 1.8 - - 3.0 - - :confval:`viewcode_follow_imported_members` - - * - ``sphinx.writers.latex.Table.caption_footnotetexts`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.Table.header_footnotetexts`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.footnotestack`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.in_container_literal_block`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.next_section_ids`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.next_hyperlink_ids`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.restrict_footnote()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.unrestrict_footnote()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.push_hyperlink_ids()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.pop_hyperlink_ids()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.bibitems`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.hlsettingstack`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.ExtBabel.get_shorthandoff()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.html.HTMLTranslator.highlightlang()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.html.HTMLTranslator.highlightlang_base()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.html.HTMLTranslator.highlightlangopts()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.html.HTMLTranslator.highlightlinenothreshold()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.html5.HTMLTranslator.highlightlang()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.html5.HTMLTranslator.highlightlang_base()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.html5.HTMLTranslator.highlightlangopts()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.html5.HTMLTranslator.highlightlinenothreshold()`` - - 1.8 - - 3.0 - - N/A - - * - ``sphinx.writers.latex.LaTeXTranslator.check_latex_elements()`` - - 1.8 - - 3.0 - - Nothing - - * - ``sphinx.application.CONFIG_FILENAME`` - - 1.8 - - 3.0 - - ``sphinx.config.CONFIG_FILENAME`` - - * - ``Config.check_unicode()`` - - 1.8 - - 3.0 - - ``sphinx.config.check_unicode()`` - - * - ``Config.check_types()`` - - 1.8 - - 3.0 - - ``sphinx.config.check_confval_types()`` - - * - ``dirname``, ``filename`` and ``tags`` arguments of - ``Config.__init__()`` - - 1.8 - - 3.0 - - ``Config.read()`` - - * - The value of :confval:`html_search_options` - - 1.8 - - 3.0 - - see :confval:`html_search_options` - - * - ``sphinx.versioning.prepare()`` - - 1.8 - - 3.0 - - ``sphinx.versioning.UIDTransform`` - - * - ``Sphinx.override_domain()`` - - 1.8 - - 3.0 - - :meth:`~sphinx.application.Sphinx.add_domain()` - - * - ``Sphinx.import_object()`` - - 1.8 - - 3.0 - - ``sphinx.util.import_object()`` - - * - ``suffix`` argument of - :meth:`~sphinx.application.Sphinx.add_source_parser()` - - 1.8 - - 3.0 - - :meth:`~sphinx.application.Sphinx.add_source_suffix()` - - - * - ``BuildEnvironment.load()`` - - 1.8 - - 3.0 - - ``pickle.load()`` - - * - ``BuildEnvironment.loads()`` - - 1.8 - - 3.0 - - ``pickle.loads()`` - - * - ``BuildEnvironment.frompickle()`` - - 1.8 - - 3.0 - - ``pickle.load()`` - - * - ``BuildEnvironment.dump()`` - - 1.8 - - 3.0 - - ``pickle.dump()`` - - * - ``BuildEnvironment.dumps()`` - - 1.8 - - 3.0 - - ``pickle.dumps()`` - - * - ``BuildEnvironment.topickle()`` - - 1.8 - - 3.0 - - ``pickle.dump()`` - - * - ``BuildEnvironment._nitpick_ignore`` - - 1.8 - - 3.0 - - :confval:`nitpick_ignore` - - * - ``BuildEnvironment.versionchanges`` - - 1.8 - - 3.0 - - N/A - - * - ``BuildEnvironment.update()`` - - 1.8 - - 3.0 - - ``Builder.read()`` - - * - ``BuildEnvironment.read_doc()`` - - 1.8 - - 3.0 - - ``Builder.read_doc()`` - - * - ``BuildEnvironment._read_serial()`` - - 1.8 - - 3.0 - - ``Builder.read()`` - - * - ``BuildEnvironment._read_parallel()`` - - 1.8 - - 3.0 - - ``Builder.read()`` - - * - ``BuildEnvironment.write_doctree()`` - - 1.8 - - 3.0 - - ``Builder.write_doctree()`` - - * - ``BuildEnvironment.note_versionchange()`` - - 1.8 - - 3.0 - - ``ChangesDomain.note_changeset()`` - - * - ``warn()`` (template helper function) - - 1.8 - - 3.0 - - ``warning()`` - - * - :confval:`source_parsers` - - 1.8 - - 3.0 - - :meth:`~sphinx.application.Sphinx.add_source_parser()` - - * - ``sphinx.util.docutils.directive_helper()`` - - 1.8 - - 3.0 - - ``Directive`` class of docutils - - * - ``sphinx.cmdline`` - - 1.8 - - 3.0 - - ``sphinx.cmd.build`` - - * - ``sphinx.make_mode`` - - 1.8 - - 3.0 - - ``sphinx.cmd.make_mode`` - - * - ``sphinx.locale.l_()`` - - 1.8 - - 3.0 - - :func:`sphinx.locale._()` - - * - ``sphinx.locale.lazy_gettext()`` - - 1.8 - - 3.0 - - :func:`sphinx.locale._()` - - * - ``sphinx.locale.mygettext()`` - - 1.8 - - 3.0 - - :func:`sphinx.locale._()` - - * - ``sphinx.util.copy_static_entry()`` - - 1.5 - - 3.0 - - ``sphinx.util.fileutil.copy_asset()`` - - * - ``sphinx.build_main()`` - - 1.7 - - 2.0 - - ``sphinx.cmd.build.build_main()`` - - * - ``sphinx.ext.intersphinx.debug()`` - - 1.7 - - 2.0 - - ``sphinx.ext.intersphinx.inspect_main()`` - - * - ``sphinx.ext.autodoc.format_annotation()`` - - 1.7 - - 2.0 - - ``sphinx.util.inspect.Signature`` - - * - ``sphinx.ext.autodoc.formatargspec()`` - - 1.7 - - 2.0 - - ``sphinx.util.inspect.Signature`` - - * - ``sphinx.ext.autodoc.AutodocReporter`` - - 1.7 - - 2.0 - - ``sphinx.util.docutils.switch_source_input()`` - - * - ``sphinx.ext.autodoc.add_documenter()`` - - 1.7 - - 2.0 - - :meth:`~sphinx.application.Sphinx.add_autodocumenter()` - - * - ``sphinx.ext.autodoc.AutoDirective._register`` - - 1.7 - - 2.0 - - :meth:`~sphinx.application.Sphinx.add_autodocumenter()` - - * - ``AutoDirective._special_attrgetters`` - - 1.7 - - 2.0 - - :meth:`~sphinx.application.Sphinx.add_autodoc_attrgetter()` - - * - ``Sphinx.warn()``, ``Sphinx.info()`` - - 1.6 - - 2.0 - - :ref:`logging-api` - - * - ``BuildEnvironment.set_warnfunc()`` - - 1.6 - - 2.0 - - :ref:`logging-api` - - * - ``BuildEnvironment.note_toctree()`` - - 1.6 - - 2.0 - - ``Toctree.note()`` (in ``sphinx.environment.adapters.toctree``) - - * - ``BuildEnvironment.get_toc_for()`` - - 1.6 - - 2.0 - - ``Toctree.get_toc_for()`` (in ``sphinx.environment.adapters.toctree``) - - * - ``BuildEnvironment.get_toctree_for()`` - - 1.6 - - 2.0 - - ``Toctree.get_toctree_for()`` (in ``sphinx.environment.adapters.toctree``) - - * - ``BuildEnvironment.create_index()`` - - 1.6 - - 2.0 - - ``IndexEntries.create_index()`` (in ``sphinx.environment.adapters.indexentries``) - - * - ``sphinx.websupport`` - - 1.6 - - 2.0 - - `sphinxcontrib-websupport `_ - - * - ``StandaloneHTMLBuilder.css_files`` - - 1.6 - - 2.0 - - :meth:`~sphinx.application.Sphinx.add_stylesheet()` - - * - ``document.settings.gettext_compact`` - - 1.8 - - 1.8 - - :confval:`gettext_compact` - - * - ``Sphinx.status_iterator()`` - - 1.6 - - 1.7 - - ``sphinx.util.status_iterator()`` - - * - ``Sphinx.old_status_iterator()`` - - 1.6 - - 1.7 - - ``sphinx.util.old_status_iterator()`` - - * - ``Sphinx._directive_helper()`` - - 1.6 - - 1.7 - - ``sphinx.util.docutils.directive_helper()`` - - * - ``sphinx.util.compat.Directive`` - - 1.6 - - 1.7 - - ``docutils.parsers.rst.Directive`` - - * - ``sphinx.util.compat.docutils_version`` - - 1.6 - - 1.7 - - ``sphinx.util.docutils.__version_info__`` - -.. note:: On deprecating on public APIs (internal functions and classes), - we also follow the policy as much as possible. + deprecated diff --git a/doc/extdev/utils.rst b/doc/extdev/utils.rst index 3aac51ed9..2a94a34bb 100644 --- a/doc/extdev/utils.rst +++ b/doc/extdev/utils.rst @@ -15,6 +15,9 @@ components (e.g. :class:`.Config`, :class:`.BuildEnvironment` and so on) easily. .. autoclass:: sphinx.transforms.SphinxTransform :members: +.. autoclass:: sphinx.transforms.post_transforms.SphinxPostTransform + :members: + .. autoclass:: sphinx.util.docutils.SphinxDirective :members: diff --git a/doc/faq.rst b/doc/faq.rst index 4b608b780..e7c23c131 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -88,7 +88,7 @@ MediaWiki Google Analytics You can use a custom ``layout.html`` template, like this: - .. code-block:: html+django + .. code-block:: html+jinja {% extends "!layout.html" %} @@ -119,6 +119,36 @@ Google Analytics {% endblock %} +Google Search + To replace Sphinx's built-in search function with Google Search, proceed as + follows: + + 1. Go to https://cse.google.com/cse/all to create the Google Search code + snippet. + + 2. Copy the code snippet and paste it into ``_templates/searchbox.html`` in + your Sphinx project: + + .. code-block:: html+jinja + +
+

{{ _('Quick search') }}

+ + +
+ + 3. Add ``searchbox.html`` to the :confval:`html_sidebars` configuration value. + .. _api role: https://git.savannah.gnu.org/cgit/kenozooid.git/tree/doc/extapi.py .. _xhtml to reST: http://docutils.sourceforge.net/sandbox/xhtml2rest/xhtml2rest.py diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index ada9efefc..31a9e0d1c 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -509,6 +509,13 @@ General configuration .. versionadded:: 1.5 + .. tip:: Sphinx uses requests_ as a HTTP library internally. + Therefore, Sphinx refers a certification file on the + directory pointed ``REQUESTS_CA_BUNDLE`` environment + variable if ``tls_cacerts`` not set. + + .. _requests: http://docs.python-requests.org/en/master/ + .. confval:: today today_fmt diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index cb41cb07d..6d7ba8272 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -315,7 +315,7 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, Configuration ------------- -There are also new config values that you can set: +There are also config values that you can set: .. confval:: autoclass_content diff --git a/doc/usage/extensions/autosummary.rst b/doc/usage/extensions/autosummary.rst index 2bf8a7d1d..891fa54ab 100644 --- a/doc/usage/extensions/autosummary.rst +++ b/doc/usage/extensions/autosummary.rst @@ -157,7 +157,7 @@ Generating stub pages automatically ----------------------------------- If you do not want to create stub pages with :program:`sphinx-autogen`, you can -also use this new config value: +also use these config values: .. confval:: autosummary_generate diff --git a/doc/usage/extensions/coverage.rst b/doc/usage/extensions/coverage.rst index 94d4c6fe4..1fb9b1850 100644 --- a/doc/usage/extensions/coverage.rst +++ b/doc/usage/extensions/coverage.rst @@ -13,7 +13,7 @@ This extension features one additional builder, the :class:`CoverageBuilder`. .. todo:: Write this section. -Several new configuration values can be used to specify what the builder +Several configuration values can be used to specify what the builder should check: .. confval:: coverage_ignore_modules diff --git a/doc/usage/extensions/extlinks.rst b/doc/usage/extensions/extlinks.rst index d818ba09b..e1d729f5c 100644 --- a/doc/usage/extensions/extlinks.rst +++ b/doc/usage/extensions/extlinks.rst @@ -18,7 +18,7 @@ tracker, at :samp:`https://github.com/sphinx-doc/sphinx/issues/{num}`. Typing this URL again and again is tedious, so you can use :mod:`~sphinx.ext.extlinks` to avoid repeating yourself. -The extension adds one new config value: +The extension adds a config value: .. confval:: extlinks diff --git a/doc/usage/extensions/graphviz.rst b/doc/usage/extensions/graphviz.rst index f87f1a4b7..bb50465ad 100644 --- a/doc/usage/extensions/graphviz.rst +++ b/doc/usage/extensions/graphviz.rst @@ -90,7 +90,7 @@ It adds these directives: .. versionadded:: 1.6 All three directives support a ``name`` option to set the label to graph. -There are also these new config values: +There are also these config values: .. confval:: graphviz_dot diff --git a/doc/usage/extensions/inheritance.rst b/doc/usage/extensions/inheritance.rst index c66f4130f..8e98b0bc1 100644 --- a/doc/usage/extensions/inheritance.rst +++ b/doc/usage/extensions/inheritance.rst @@ -25,12 +25,18 @@ It adds this directive: graph. This directive supports an option called ``parts`` that, if given, must be an - integer, advising the directive to remove that many parts of module names - from the displayed names. (For example, if all your class names start with - ``lib.``, you can give ``:parts: 1`` to remove that prefix from the displayed - node names.) + integer, advising the directive to keep that many dot-separated parts + in the displayed names (from right to left). For example, ``parts=1`` will + only display class names, without the names of the modules that contain + them. - It also supports a ``private-bases`` flag option; if given, private base + .. versionchanged:: 2.0 + The value of for ``parts`` can also be negative, indicating how many + parts to drop from the left. For example, if all your class names start + with ``lib.``, you can give ``:parts: -1`` to remove that prefix from the + displayed node names. + + The directive also supports a ``private-bases`` flag option; if given, private base classes (those whose name starts with ``_``) will be included. You can use ``caption`` option to give a caption to the diagram. @@ -92,6 +98,41 @@ It adds this directive: Added ``top-classes`` option to limit the scope of inheritance graphs. +Examples +-------- + +The following are different inheritance diagrams for the internal +``InheritanceDiagram`` class that implements the directive. + +With full names:: + + .. inheritance-diagram:: sphinx.ext.inheritance_diagram.InheritanceDiagram + +.. inheritance-diagram:: sphinx.ext.inheritance_diagram.InheritanceDiagram + + +Showing class names only:: + + .. inheritance-diagram:: sphinx.ext.inheritance_diagram.InheritanceDiagram + :parts: 1 + +.. inheritance-diagram:: sphinx.ext.inheritance_diagram.InheritanceDiagram + :parts: 1 + +Stopping the diagram at :class:`sphinx.util.docutils.SphinxDirective` (the +highest superclass still part of Sphinx), and dropping the common left-most +part (``sphinx``) from all names:: + + .. inheritance-diagram:: sphinx.ext.inheritance_diagram.InheritanceDiagram + :top-classes: sphinx.util.docutils.SphinxDirective + :parts: -1 + +.. inheritance-diagram:: sphinx.ext.inheritance_diagram.InheritanceDiagram + :top-classes: sphinx.util.docutils.SphinxDirective + :parts: -1 + + + Configuration ------------- diff --git a/doc/usage/extensions/intersphinx.rst b/doc/usage/extensions/intersphinx.rst index cfda53e8f..0b2070400 100644 --- a/doc/usage/extensions/intersphinx.rst +++ b/doc/usage/extensions/intersphinx.rst @@ -43,7 +43,7 @@ Configuration ------------- To use Intersphinx linking, add ``'sphinx.ext.intersphinx'`` to your -:confval:`extensions` config value, and use these new config values to activate +:confval:`extensions` config value, and use these config values to activate linking: .. confval:: intersphinx_mapping diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 3ac90b5fb..d0da75d4d 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -1084,7 +1084,7 @@ Overloaded (member) functions When a (member) function is referenced using just its name, the reference will point to an arbitrary matching overload. -The :rst:role:`cpp:any` and :rst:role:`cpp:func` roles will an alternative +The :rst:role:`cpp:any` and :rst:role:`cpp:func` roles use an alternative format, which simply is a complete function declaration. This will resolve to the exact matching overload. As example, consider the following class declaration: @@ -1195,6 +1195,7 @@ Configuration Variables See :ref:`cpp-config`. +.. _domains-std: The Standard Domain ------------------- @@ -1294,8 +1295,6 @@ The JavaScript domain (name **js**) provides the following directives: specified. If this option is specified, the directive will only update the current module name. - To clear the current module, set the module name to ``null`` or ``None`` - .. versionadded:: 1.6 .. rst:directive:: .. js:function:: name(signature) diff --git a/setup.py b/setup.py index 79c466321..30f8625c8 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,6 @@ extras_require = { 'colorama>=0.3.5', ], 'test': [ - 'mock', 'pytest', 'pytest-cov', 'html5lib', @@ -173,6 +172,7 @@ setup( author_email='georg@python.org', description='Python documentation generator', long_description=long_desc, + long_description_content_type='text/x-rst', zip_safe=False, classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/sphinx/__init__.py b/sphinx/__init__.py index 0a6f26584..1c8338643 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -32,8 +32,8 @@ if 'PYTHONWARNINGS' not in os.environ: warnings.filterwarnings('ignore', "'U' mode is deprecated", DeprecationWarning, module='docutils.io') -__version__ = '2.1.0+' -__released__ = '2.1.0' # used when Sphinx builds its own docs +__version__ = '3.0.0+' +__released__ = '3.0.0' # used when Sphinx builds its own docs #: Version info for better programmatic use. #: @@ -43,7 +43,7 @@ __released__ = '2.1.0' # used when Sphinx builds its own docs #: #: .. versionadded:: 1.2 #: Before version 1.2, check the string ``sphinx.__version__``. -version_info = (2, 1, 0, 'beta', 0) +version_info = (3, 0, 0, 'beta', 0) package_dir = path.abspath(path.dirname(__file__)) diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index ef3bf3f9e..4180625ca 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -12,7 +12,7 @@ import warnings from docutils import nodes -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning if False: # For type annotation @@ -188,59 +188,6 @@ class production(nodes.Part, nodes.Inline, nodes.FixedTextElement): """Node for a single grammar production rule.""" -# math nodes - - -class math(nodes.math): - """Node for inline equations. - - .. warning:: This node is provided to keep compatibility only. - It will be removed in nearly future. Don't use this from your extension. - - .. deprecated:: 1.8 - Use ``docutils.nodes.math`` instead. - """ - - def __getitem__(self, key): - """Special accessor for supporting ``node['latex']``.""" - if key == 'latex' and 'latex' not in self.attributes: - warnings.warn("math node for Sphinx was replaced by docutils'. " - "Therefore please use ``node.astext()`` to get an equation instead.", - RemovedInSphinx30Warning, stacklevel=2) - return self.astext() - else: - return super().__getitem__(key) - - -class math_block(nodes.math_block): - """Node for block level equations. - - .. warning:: This node is provided to keep compatibility only. - It will be removed in nearly future. Don't use this from your extension. - - .. deprecated:: 1.8 - """ - - def __getitem__(self, key): - if key == 'latex' and 'latex' not in self.attributes: - warnings.warn("displaymath node for Sphinx was replaced by docutils'. " - "Therefore please use ``node.astext()`` to get an equation instead.", - RemovedInSphinx30Warning, stacklevel=2) - return self.astext() - else: - return super().__getitem__(key) - - -class displaymath(math_block): - """Node for block level equations. - - .. warning:: This node is provided to keep compatibility only. - It will be removed in nearly future. Don't use this from your extension. - - .. deprecated:: 1.8 - """ - - # other directive-level nodes class index(nodes.Invisible, nodes.Inline, nodes.TextElement): @@ -379,7 +326,6 @@ def setup(app): app.add_node(seealso) app.add_node(productionlist) app.add_node(production) - app.add_node(displaymath) app.add_node(index) app.add_node(centered) app.add_node(acks) diff --git a/sphinx/application.py b/sphinx/application.py index 6d553333b..516b7be58 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -15,7 +15,6 @@ import pickle import sys import warnings from collections import deque -from inspect import isclass from io import StringIO from os import path @@ -25,9 +24,7 @@ import sphinx from sphinx import package_dir, locale from sphinx.config import Config from sphinx.config import CONFIG_FILENAME # NOQA # for compatibility (RemovedInSphinx30) -from sphinx.deprecation import ( - RemovedInSphinx30Warning, RemovedInSphinx40Warning -) +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.environment import BuildEnvironment from sphinx.errors import ApplicationError, ConfigError, VersionRequirementError from sphinx.events import EventManager @@ -35,12 +32,11 @@ from sphinx.locale import __ from sphinx.project import Project from sphinx.registry import SphinxComponentRegistry from sphinx.util import docutils -from sphinx.util import import_object, progress_message from sphinx.util import logging +from sphinx.util import progress_message from sphinx.util.build_phase import BuildPhase from sphinx.util.console import bold # type: ignore -from sphinx.util.docutils import directive_helper -from sphinx.util.i18n import find_catalog_source_files +from sphinx.util.i18n import CatalogRepository from sphinx.util.logging import prefixed_warnings from sphinx.util.osutil import abspath, ensuredir, relpath from sphinx.util.tags import Tags @@ -91,11 +87,15 @@ builtin_extensions = ( 'sphinx.parsers', 'sphinx.registry', 'sphinx.roles', + 'sphinx.transforms', + 'sphinx.transforms.compact_bullet_list', + 'sphinx.transforms.i18n', + 'sphinx.transforms.references', 'sphinx.transforms.post_transforms', 'sphinx.transforms.post_transforms.code', 'sphinx.transforms.post_transforms.images', - 'sphinx.transforms.post_transforms.compat', 'sphinx.util.compat', + 'sphinx.versioning', # collectors should be loaded by specific order 'sphinx.environment.collectors.dependencies', 'sphinx.environment.collectors.asset', @@ -265,21 +265,21 @@ class Sphinx: """Load translated strings from the configured localedirs if enabled in the configuration. """ - if self.config.language is not None: + if self.config.language is None: + self.translator, has_translation = locale.init([], None) + else: logger.info(bold(__('loading translations [%s]... ') % self.config.language), nonl=True) - user_locale_dirs = [ - path.join(self.srcdir, x) for x in self.config.locale_dirs] + # compile mo files if sphinx.po file in user locale directories are updated - for catinfo in find_catalog_source_files( - user_locale_dirs, self.config.language, domains=['sphinx'], - charset=self.config.source_encoding): - catinfo.write_mo(self.config.language) - locale_dirs = [None, path.join(package_dir, 'locale')] + user_locale_dirs - else: - locale_dirs = [] - self.translator, has_translation = locale.init(locale_dirs, self.config.language) - if self.config.language is not None: + repo = CatalogRepository(self.srcdir, self.config.locale_dirs, + self.config.language, self.config.source_encoding) + for catalog in repo.catalogs: + if catalog.domain == 'sphinx' and catalog.is_outdated(): + catalog.write_mo(self.config.language) + + locale_dirs = [None, path.join(package_dir, 'locale')] + list(repo.locale_dirs) + self.translator, has_translation = locale.init(locale_dirs, self.config.language) if has_translation or self.config.language == 'en': # "en" never needs to be translated logger.info(__('done')) @@ -392,18 +392,6 @@ class Sphinx: if version > sphinx.__display_version__[:3]: raise VersionRequirementError(version) - def import_object(self, objname, source=None): - # type: (str, str) -> Any - """Import an object from a ``module.name`` string. - - .. deprecated:: 1.8 - Use ``sphinx.util.import_object()`` instead. - """ - warnings.warn('app.import_object() is deprecated. ' - 'Use sphinx.util.add_object_type() instead.', - RemovedInSphinx30Warning, stacklevel=2) - return import_object(objname, source=None) - # event interface def connect(self, event, callback): # type: (str, Callable) -> int @@ -588,36 +576,14 @@ class Sphinx: self.registry.add_enumerable_node(node, figtype, title_getter, override=override) self.add_node(node, override=override, **kwds) - @property - def enumerable_nodes(self): - # type: () -> Dict[Type[nodes.Node], Tuple[str, TitleGetter]] - warnings.warn('app.enumerable_nodes() is deprecated. ' - 'Use app.get_domain("std").enumerable_nodes instead.', - RemovedInSphinx30Warning, stacklevel=2) - return self.registry.enumerable_nodes - - def add_directive(self, name, obj, content=None, arguments=None, override=False, **options): # NOQA - # type: (str, Any, bool, Tuple[int, int, bool], bool, Any) -> None + def add_directive(self, name, cls, override=False): + # type: (str, Type[Directive], bool) -> None """Register a Docutils directive. - *name* must be the prospective directive name. There are two possible - ways to write a directive: - - - In the docutils 0.4 style, *obj* is the directive function. - *content*, *arguments* and *options* are set as attributes on the - function and determine whether the directive has content, arguments - and options, respectively. **This style is deprecated.** - - - In the docutils 0.5 style, *obj* is the directive class. - It must already have attributes named *has_content*, - *required_arguments*, *optional_arguments*, - *final_argument_whitespace* and *option_spec* that correspond to the - options for the function way. See `the Docutils docs - `_ - for details. - - The directive class must inherit from the class - ``docutils.parsers.rst.Directive``. + *name* must be the prospective directive name. *cls* is a directive + class which inherits ``docutils.parsers.rst.Directive``. For more + details, see `the Docutils docs + `_ . For example, the (already existing) :rst:dir:`literalinclude` directive would be added like this: @@ -648,17 +614,12 @@ class Sphinx: .. versionchanged:: 1.8 Add *override* keyword. """ - logger.debug('[app] adding directive: %r', - (name, obj, content, arguments, options)) + logger.debug('[app] adding directive: %r', (name, cls)) if not override and docutils.is_directive_registered(name): logger.warning(__('directive %r is already registered, it will be overridden'), name, type='app', subtype='add_directive') - if not isclass(obj) or not issubclass(obj, Directive): - directive = directive_helper(obj, content, arguments, **options) - docutils.register_directive(name, directive) - else: - docutils.register_directive(name, obj) + docutils.register_directive(name, cls) def add_role(self, name, role, override=False): # type: (str, Any, bool) -> None @@ -711,26 +672,8 @@ class Sphinx: """ self.registry.add_domain(domain, override=override) - def override_domain(self, domain): - # type: (Type[Domain]) -> None - """Override a registered domain. - - Make the given *domain* class known to Sphinx, assuming that there is - already a domain with its ``.name``. The new domain must be a subclass - of the existing one. - - .. versionadded:: 1.0 - .. deprecated:: 1.8 - Integrated to :meth:`add_domain`. - """ - warnings.warn('app.override_domain() is deprecated. ' - 'Use app.add_domain() with override option instead.', - RemovedInSphinx30Warning, stacklevel=2) - self.registry.add_domain(domain, override=True) - - def add_directive_to_domain(self, domain, name, obj, has_content=None, argument_spec=None, - override=False, **option_spec): - # type: (str, str, Any, bool, Any, bool, Any) -> None + def add_directive_to_domain(self, domain, name, cls, override=False): + # type: (str, str, Type[Directive], bool) -> None """Register a Docutils directive in a domain. Like :meth:`add_directive`, but the directive is added to the domain @@ -740,9 +683,7 @@ class Sphinx: .. versionchanged:: 1.8 Add *override* keyword. """ - self.registry.add_directive_to_domain(domain, name, obj, - has_content, argument_spec, override=override, - **option_spec) + self.registry.add_directive_to_domain(domain, name, cls, override=override) def add_role_to_domain(self, domain, name, role, override=False): # type: (str, str, Union[RoleFunction, XRefRole], bool) -> None @@ -827,9 +768,6 @@ class Sphinx: For the role content, you have the same syntactical possibilities as for standard Sphinx roles (see :ref:`xref-syntax`). - This method is also available under the deprecated alias - :meth:`add_description_unit`. - .. versionchanged:: 1.8 Add *override* keyword. """ @@ -1203,13 +1141,6 @@ class Sphinx: return True - @property - def _setting_up_extension(self): - # type: () -> List[str] - warnings.warn('app._setting_up_extension is deprecated.', - RemovedInSphinx30Warning) - return ['?'] - class TemplateBridge: """ diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 335880ef0..1b29fa983 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -19,12 +19,11 @@ from sphinx.environment.adapters.asset import ImageAdapter from sphinx.errors import SphinxError from sphinx.io import read_doc from sphinx.locale import __ -from sphinx.util import i18n, import_object, logging, rst, progress_message, status_iterator +from sphinx.util import import_object, logging, rst, progress_message, status_iterator from sphinx.util.build_phase import BuildPhase from sphinx.util.console import bold # type: ignore from sphinx.util.docutils import sphinx_domains -from sphinx.util.i18n import find_catalog -from sphinx.util.matching import Matcher +from sphinx.util.i18n import CatalogRepository, docname_to_domain from sphinx.util.osutil import SEP, ensuredir, relative_uri, relpath from sphinx.util.parallel import ParallelTasks, SerialTasks, make_chunks, \ parallel_available @@ -40,7 +39,7 @@ except ImportError: if False: # For type annotation - from typing import Any, Callable, Dict, Iterable, List, Sequence, Set, Tuple, Type, Union # NOQA + from typing import Any, Dict, Iterable, List, Sequence, Set, Tuple, Type, Union # NOQA from sphinx.application import Sphinx # NOQA from sphinx.config import Config # NOQA from sphinx.environment import BuildEnvironment # NOQA @@ -236,14 +235,10 @@ class Builder: def compile_all_catalogs(self): # type: () -> None - catalogs = i18n.find_catalog_source_files( - [path.join(self.srcdir, x) for x in self.config.locale_dirs], - self.config.language, - charset=self.config.source_encoding, - force_all=True, - excluded=Matcher(['**/.?**'])) - message = __('all of %d po files') % len(catalogs) - self.compile_catalogs(catalogs, message) + repo = CatalogRepository(self.srcdir, self.config.locale_dirs, + self.config.language, self.config.source_encoding) + message = __('all of %d po files') % len(list(repo.catalogs)) + self.compile_catalogs(set(repo.catalogs), message) def compile_specific_catalogs(self, specified_files): # type: (List[str]) -> None @@ -251,28 +246,25 @@ class Builder: # type: (str) -> str docname = self.env.path2doc(path.abspath(fpath)) if docname: - return find_catalog(docname, self.config.gettext_compact) + return docname_to_domain(docname, self.config.gettext_compact) else: return None - specified_domains = set(map(to_domain, specified_files)) - specified_domains.discard(None) - catalogs = i18n.find_catalog_source_files( - [path.join(self.srcdir, x) for x in self.config.locale_dirs], - self.config.language, - domains=list(specified_domains), - charset=self.config.source_encoding, - excluded=Matcher(['**/.?**'])) + catalogs = set() + domains = set(map(to_domain, specified_files)) + repo = CatalogRepository(self.srcdir, self.config.locale_dirs, + self.config.language, self.config.source_encoding) + for catalog in repo.catalogs: + if catalog.domain in domains and catalog.is_outdated(): + catalogs.add(catalog) message = __('targets for %d po files that are specified') % len(catalogs) self.compile_catalogs(catalogs, message) def compile_update_catalogs(self): # type: () -> None - catalogs = i18n.find_catalog_source_files( - [path.join(self.srcdir, x) for x in self.config.locale_dirs], - self.config.language, - charset=self.config.source_encoding, - excluded=Matcher(['**/.?**'])) + repo = CatalogRepository(self.srcdir, self.config.locale_dirs, + self.config.language, self.config.source_encoding) + catalogs = {c for c in repo.catalogs if c.is_outdated()} message = __('targets for %d po files that are out of date') % len(catalogs) self.compile_catalogs(catalogs, message) @@ -298,8 +290,7 @@ class Builder: logger.warning(__('file %r given on command line is not under the ' 'source directory, ignoring'), filename) continue - if not (path.isfile(filename) or - any(path.isfile(filename + suffix) for suffix in suffixes)): + if not path.isfile(filename): logger.warning(__('file %r given on command line does not exist, ' 'ignoring'), filename) continue diff --git a/sphinx/builders/_epub_base.py b/sphinx/builders/_epub_base.py index 00fa7187d..90ab6c12d 100644 --- a/sphinx/builders/_epub_base.py +++ b/sphinx/builders/_epub_base.py @@ -36,7 +36,6 @@ except ImportError: if False: # For type annotation from typing import Any, Dict, List, Set, Tuple # NOQA - from sphinx.application import Sphinx # NOQA logger = logging.getLogger(__name__) diff --git a/sphinx/builders/changes.py b/sphinx/builders/changes.py index d5c4e6bc0..3b169e493 100644 --- a/sphinx/builders/changes.py +++ b/sphinx/builders/changes.py @@ -83,8 +83,7 @@ class ChangesBuilder(Builder): entry = '%s: %s.' % (descname, ttext) apichanges.append((entry, changeset.docname, changeset.lineno)) elif descname or changeset.module: - if not changeset.module: - module = _('Builtins') + module = changeset.module or _('Builtins') if not descname: descname = _('Module level') if context: @@ -149,8 +148,8 @@ class ChangesBuilder(Builder): 'text': text } f.write(self.templates.render('changes/rstsource.html', ctx)) - themectx = dict(('theme_' + key, val) for (key, val) in - self.theme.get_options({}).items()) + themectx = {'theme_' + key: val for (key, val) in + self.theme.get_options({}).items()} copy_asset_file(path.join(package_dir, 'themes', 'default', 'static', 'default.css_t'), self.outdir, context=themectx, renderer=self.templates) copy_asset_file(path.join(package_dir, 'themes', 'basic', 'static', 'basic.css'), diff --git a/sphinx/builders/epub3.py b/sphinx/builders/epub3.py index f97d96396..3116cd493 100644 --- a/sphinx/builders/epub3.py +++ b/sphinx/builders/epub3.py @@ -25,8 +25,7 @@ from sphinx.util.osutil import make_filename if False: # For type annotation - from typing import Any, Dict, Iterable, List, Set, Tuple # NOQA - from docutils import nodes # NOQA + from typing import Any, Dict, List, Set, Tuple # NOQA from sphinx.application import Sphinx # NOQA from sphinx.config import Config # NOQA diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index aace9bb49..f26b831da 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -16,13 +16,14 @@ from os import path, walk, getenv from time import time from uuid import uuid4 +from sphinx import addnodes from sphinx.builders import Builder from sphinx.domains.python import pairindextypes from sphinx.errors import ThemeError from sphinx.locale import __ from sphinx.util import split_index_msg, logging, status_iterator from sphinx.util.console import bold # type: ignore -from sphinx.util.i18n import find_catalog +from sphinx.util.i18n import docname_to_domain from sphinx.util.nodes import extract_messages, traverse_translatable_index from sphinx.util.osutil import relpath, ensuredir, canon_path from sphinx.util.tags import Tags @@ -140,7 +141,12 @@ class I18nBuilder(Builder): def write_doc(self, docname, doctree): # type: (str, nodes.document) -> None - catalog = self.catalogs[find_catalog(docname, self.config.gettext_compact)] + catalog = self.catalogs[docname_to_domain(docname, self.config.gettext_compact)] + + for toctree in self.env.tocs[docname].traverse(addnodes.toctree): + for node, msg in extract_messages(toctree): + node.uid = '' # type: ignore # Hack UUID model + catalog.add(msg, node) for node, msg in extract_messages(doctree): catalog.add(msg, node) diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 2c5ebcd4d..5621f9a75 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -24,9 +24,7 @@ from docutils.utils import relative_path from sphinx import package_dir, __display_version__ from sphinx.builders import Builder -from sphinx.deprecation import ( - RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias -) +from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.environment.adapters.asset import ImageAdapter from sphinx.environment.adapters.indexentries import IndexEntries from sphinx.environment.adapters.toctree import TocTree @@ -105,39 +103,6 @@ class Stylesheet(str): return self -class JSContainer(list): - """The container for JavaScript scripts.""" - def insert(self, index, obj): - # type: (int, str) -> None - warnings.warn('To modify script_files in the theme is deprecated. ' - 'Please insert a {%- endblock %} {% block extrahead %} - - {# this is used when loading the search index using $.ajax fails, - such as on Chrome for documents on localhost #} - + {{ super() }} {% endblock %} {% block body %} diff --git a/sphinx/themes/basic/static/basic.css_t b/sphinx/themes/basic/static/basic.css_t index ec30e8432..90a14286f 100644 --- a/sphinx/themes/basic/static/basic.css_t +++ b/sphinx/themes/basic/static/basic.css_t @@ -401,11 +401,13 @@ table.citation td { border-bottom: none; } +th > p:first-child, td > p:first-child { margin-top: 0px; } -td > p:only-child { +th > p:last-child, +td > p:last-child { margin-bottom: 0px; } @@ -482,7 +484,7 @@ li > p:first-child { margin-top: 0px; } -li > p:only-child { +li > p:last-child { margin-bottom: 0px; } diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index bdc270655..4c5826411 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -75,16 +75,6 @@ var Search = { } }, - loadIndex : function(url) { - $.ajax({type: "GET", url: url, data: null, - dataType: "script", cache: true, - complete: function(jqxhr, textstatus) { - if (textstatus != "success") { - document.getElementById("searchindexloader").src = url; - } - }}); - }, - setIndex : function(index) { var q; this._index = index; diff --git a/sphinx/theming.py b/sphinx/theming.py index f669adabf..669e71dde 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) if False: # For type annotation - from typing import Any, Dict, Iterator, List, Tuple # NOQA + from typing import Any, Dict, List # NOQA from sphinx.application import Sphinx # NOQA NODEFAULT = object() diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 890cfcb84..2fc1b6e72 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -23,11 +23,13 @@ from sphinx.locale import _, __ from sphinx.util import logging from sphinx.util.docutils import new_document from sphinx.util.i18n import format_date -from sphinx.util.nodes import NodeMatcher, apply_source_workaround, is_smartquotable +from sphinx.util.nodes import ( + NodeMatcher, apply_source_workaround, copy_source_info, is_smartquotable +) if False: # For type annotation - from typing import Any, Generator, List, Tuple # NOQA + from typing import Any, Dict, Generator, List, Tuple # NOQA from sphinx.application import Sphinx # NOQA from sphinx.config import Config # NOQA from sphinx.domain.std import StandardDomain # NOQA @@ -36,11 +38,11 @@ if False: logger = logging.getLogger(__name__) -default_substitutions = set([ +default_substitutions = { 'version', 'release', 'today', -]) +} class SphinxTransform(Transform): @@ -198,6 +200,18 @@ class SortIds(SphinxTransform): node['ids'] = node['ids'][1:] + [node['ids'][0]] +class SmartQuotesSkipper(SphinxTransform): + """Mark specific nodes as not smartquoted.""" + default_priority = 619 + + def apply(self, **kwargs): + # type: (Any) -> None + # citation labels + for node in self.document.traverse(nodes.citation): + label = cast(nodes.label, node[0]) + label['support_smartquotes'] = False + + class CitationReferences(SphinxTransform): """ Replace citation references by pending_xref nodes before the default @@ -207,21 +221,16 @@ class CitationReferences(SphinxTransform): def apply(self, **kwargs): # type: (Any) -> None - # mark citation labels as not smartquoted - for citation in self.document.traverse(nodes.citation): - label = cast(nodes.label, citation[0]) - label['support_smartquotes'] = False - - for citation_ref in self.document.traverse(nodes.citation_reference): - cittext = citation_ref.astext() - refnode = addnodes.pending_xref(cittext, refdomain='std', reftype='citation', - reftarget=cittext, refwarn=True, - support_smartquotes=False, - ids=citation_ref["ids"]) - refnode.source = citation_ref.source or citation_ref.parent.source - refnode.line = citation_ref.line or citation_ref.parent.line - refnode += nodes.Text('[' + cittext + ']') - citation_ref.parent.replace(citation_ref, refnode) + for node in self.document.traverse(nodes.citation_reference): + target = node.astext() + ref = addnodes.pending_xref(target, refdomain='std', reftype='citation', + reftarget=target, refwarn=True, + support_smartquotes=False, + ids=node["ids"], + classes=node.get('classes', [])) + ref += nodes.inline(target, '[%s]' % target) + copy_source_info(node, ref) + node.replace_self(ref) TRANSLATABLE_NODES = { @@ -340,11 +349,7 @@ class SphinxContentsFilter(ContentsFilter): Used with BuildEnvironment.add_toc_from() to discard cross-file links within table-of-contents link nodes. """ - def visit_pending_xref(self, node): - # type: (addnodes.pending_xref) -> None - text = node.astext() - self.parent.append(nodes.literal(text, text)) - raise nodes.SkipNode + visit_pending_xref = ContentsFilter.ignore_node_but_process_children def visit_image(self, node): # type: (nodes.image) -> None @@ -433,3 +438,29 @@ class ManpageLink(SphinxTransform): if r: info = r.groupdict() node.attributes.update(info) + + +def setup(app): + # type: (Sphinx) -> Dict[str, Any] + app.add_transform(ApplySourceWorkaround) + app.add_transform(ExtraTranslatableNodes) + app.add_transform(SmartQuotesSkipper) + app.add_transform(CitationReferences) + app.add_transform(DefaultSubstitutions) + app.add_transform(MoveModuleTargets) + app.add_transform(HandleCodeBlocks) + app.add_transform(SortIds) + app.add_transform(FigureAligner) + app.add_transform(AutoNumbering) + app.add_transform(AutoIndexUpgrader) + app.add_transform(FilterSystemMessages) + app.add_transform(UnreferencedFootnotesDetector) + app.add_transform(SphinxSmartQuotes) + app.add_transform(DoctreeReadEvent) + app.add_transform(ManpageLink) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/transforms/compact_bullet_list.py b/sphinx/transforms/compact_bullet_list.py index 7f7db5c40..e118e6a84 100644 --- a/sphinx/transforms/compact_bullet_list.py +++ b/sphinx/transforms/compact_bullet_list.py @@ -17,7 +17,8 @@ from sphinx.transforms import SphinxTransform if False: # For type annotation - from typing import Any, List # NOQA + from typing import Any, Dict, List # NOQA + from sphinx.application import Sphinx # NOQA class RefOnlyListChecker(nodes.GenericNodeVisitor): @@ -90,3 +91,14 @@ class RefOnlyBulletListTransform(SphinxTransform): compact_para = addnodes.compact_paragraph() compact_para += ref item.replace(para, compact_para) + + +def setup(app): + # type: (Sphinx) -> Dict[str, Any] + app.add_transform(RefOnlyBulletListTransform) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 494dff855..c884bb89d 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -21,7 +21,7 @@ from sphinx.domains.std import make_glossary_term, split_term_classifiers from sphinx.locale import __, init as init_locale from sphinx.transforms import SphinxTransform from sphinx.util import split_index_msg, logging -from sphinx.util.i18n import find_catalog +from sphinx.util.i18n import docname_to_domain from sphinx.util.nodes import ( LITERAL_TYPE_NODES, IMAGE_TYPE_NODES, NodeMatcher, extract_messages, is_pending_meta, traverse_translatable_index, @@ -94,7 +94,7 @@ class Locale(SphinxTransform): assert source.startswith(self.env.srcdir) docname = path.splitext(relative_path(path.join(self.env.srcdir, 'dummy'), source))[0] - textdomain = find_catalog(docname, self.config.gettext_compact) + textdomain = docname_to_domain(docname, self.config.gettext_compact) # fetch translations dirs = [path.join(self.env.srcdir, directory) @@ -483,3 +483,16 @@ class RemoveTranslatableInline(SphinxTransform): for inline in self.document.traverse(matcher): # type: nodes.inline inline.parent.remove(inline) inline.parent += inline.children + + +def setup(app): + # type: (Sphinx) -> Dict[str, Any] + app.add_transform(PreserveTranslatableMessages) + app.add_transform(Locale) + app.add_transform(RemoveTranslatableInline) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index 60be2fe05..8f64266d0 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -13,7 +13,7 @@ from typing import cast from docutils import nodes from sphinx import addnodes -from sphinx.environment import NoUri +from sphinx.errors import NoUri from sphinx.locale import __ from sphinx.transforms import SphinxTransform from sphinx.util import logging @@ -29,14 +29,48 @@ if False: logger = logging.getLogger(__name__) -class ReferencesResolver(SphinxTransform): +class SphinxPostTransform(SphinxTransform): + """A base class of post-transforms. + + Post transforms are invoked to modify the document to restructure it for outputting. + They do resolving references, convert images, special transformation for each output + formats and so on. This class helps to implement these post transforms. + """ + builders = () # type: Tuple[str, ...] + formats = () # type: Tuple[str, ...] + + def apply(self, **kwargs): + # type: (Any) -> None + if self.is_supported(): + self.run(**kwargs) + + def is_supported(self): + # type: () -> bool + """Check this transform working for current builder.""" + if self.builders and self.app.builder.name not in self.builders: + return False + if self.formats and self.app.builder.format not in self.formats: + return False + + return True + + def run(self, **kwargs): + # type: (Any) -> None + """main method of post transforms. + + Subclasses should override this method instead of ``apply()``. + """ + raise NotImplementedError + + +class ReferencesResolver(SphinxPostTransform): """ Resolves cross-references on doctrees. """ default_priority = 10 - def apply(self, **kwargs): + def run(self, **kwargs): # type: (Any) -> None for node in self.document.traverse(addnodes.pending_xref): contnode = cast(nodes.TextElement, node[0].deepcopy()) @@ -147,10 +181,10 @@ class ReferencesResolver(SphinxTransform): location=node, type='ref', subtype=typ) -class OnlyNodeTransform(SphinxTransform): +class OnlyNodeTransform(SphinxPostTransform): default_priority = 50 - def apply(self, **kwargs): + def run(self, **kwargs): # type: (Any) -> None # A comment on the comment() nodes being inserted: replacing by [] would # result in a "Losing ids" exception if there is a target node before diff --git a/sphinx/transforms/post_transforms/compat.py b/sphinx/transforms/post_transforms/compat.py deleted file mode 100644 index d1e8aedd5..000000000 --- a/sphinx/transforms/post_transforms/compat.py +++ /dev/null @@ -1,94 +0,0 @@ -""" - sphinx.transforms.post_transforms.compat - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Post transforms for compatibility - - :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -import warnings - -from docutils import nodes -from docutils.writers.docutils_xml import XMLTranslator - -from sphinx.addnodes import math_block, displaymath -from sphinx.deprecation import RemovedInSphinx30Warning -from sphinx.transforms import SphinxTransform -from sphinx.util import logging - -if False: - # For type annotation - from typing import Any, Callable, Dict, Iterable, List, Tuple # NOQA - from docutils.parsers.rst.states import Inliner # NOQA - from docutils.writers.html4css1 import Writer # NOQA - from sphinx.application import Sphinx # NOQA - from sphinx.builders import Builder # NOQA - from sphinx.environment import BuildEnvironment # NOQA - -logger = logging.getLogger(__name__) - - -class MathNodeMigrator(SphinxTransform): - """Migrate a math node to docutils'. - - For a long time, Sphinx uses an original node for math. Since 1.8, - Sphinx starts to use a math node of docutils'. This transform converts - old and new nodes to keep compatibility. - """ - default_priority = 999 - - def apply(self, **kwargs): - # type: (Any) -> None - for math_node in self.document.traverse(nodes.math): - # case: old styled ``math`` node generated by old extensions - if len(math_node) == 0: - warnings.warn("math node for Sphinx was replaced by docutils'. " - "Please use ``docutils.nodes.math`` instead.", - RemovedInSphinx30Warning) - equation = math_node['latex'] - math_node += nodes.Text(equation, equation) - - translator = self.app.builder.get_translator_class() - if hasattr(translator, 'visit_displaymath') and translator != XMLTranslator: - # case: old translators which does not support ``math_block`` node - warnings.warn("Translator for %s does not support math_block node'. " - "Please update your extension." % translator, - RemovedInSphinx30Warning) - for old_math_block_node in self.document.traverse(math_block): - alt = displaymath(latex=old_math_block_node.astext(), - number=old_math_block_node.get('number'), - label=old_math_block_node.get('label'), - nowrap=old_math_block_node.get('nowrap'), - docname=old_math_block_node.get('docname')) - old_math_block_node.replace_self(alt) - elif getattr(self.app.builder, 'math_renderer_name', None) == 'unknown': - # case: math extension provides old styled math renderer - for math_block_node in self.document.traverse(nodes.math_block): - math_block_node['latex'] = math_block_node.astext() - - # case: old styled ``displaymath`` node generated by old extensions - for math_block_node in self.document.traverse(math_block): - if len(math_block_node) == 0: - warnings.warn("math node for Sphinx was replaced by docutils'. " - "Please use ``docutils.nodes.math_block`` instead.", - RemovedInSphinx30Warning) - if isinstance(math_block_node, displaymath): - newnode = nodes.math_block('', math_block_node['latex'], - **math_block_node.attributes) - math_block_node.replace_self(newnode) - else: - latex = math_block_node['latex'] - math_block_node += nodes.Text(latex, latex) - - -def setup(app): - # type: (Sphinx) -> Dict[str, Any] - app.add_post_transform(MathNodeMigrator) - - return { - 'version': 'builtin', - 'parallel_read_safe': True, - 'parallel_write_safe': True, - } diff --git a/sphinx/transforms/references.py b/sphinx/transforms/references.py index fd7e71779..de512f437 100644 --- a/sphinx/transforms/references.py +++ b/sphinx/transforms/references.py @@ -15,7 +15,8 @@ from sphinx.transforms import SphinxTransform if False: # For type annotation - from typing import Any # NOQA + from typing import Any, Dict # NOQA + from sphinx.application import Sphinx # NOQA class SubstitutionDefinitionsRemover(SphinxTransform): @@ -38,3 +39,15 @@ class SphinxDomains(SphinxTransform): # type: (Any) -> None for domain in self.env.domains.values(): domain.process_doc(self.env, self.env.docname, self.document) + + +def setup(app): + # type: (Sphinx) -> Dict[str, Any] + app.add_transform(SubstitutionDefinitionsRemover) + app.add_transform(SphinxDomains) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index bfe99778a..cc2cd9aa1 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -26,21 +26,18 @@ from os import path from time import mktime, strptime from urllib.parse import urlsplit, urlunsplit, quote_plus, parse_qsl, urlencode -from docutils.utils import relative_path - -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import PycodeError, SphinxParallelError, ExtensionError from sphinx.locale import __ from sphinx.util import logging from sphinx.util.console import strip_colors, colorize, bold, term_width_line # type: ignore -from sphinx.util.fileutil import copy_asset_file from sphinx.util import smartypants # noqa # import other utilities; partly for backwards compatibility, so don't # prune unused ones indiscriminately from sphinx.util.osutil import ( # noqa SEP, os_path, relative_uri, ensuredir, walk, mtimes_of_files, movefile, - copyfile, copytimes, make_filename, ustrftime) + copyfile, copytimes, make_filename) from sphinx.util.nodes import ( # noqa nested_parse_with_titles, split_explicit_title, explicit_title_re, caption_ref_re) @@ -48,7 +45,7 @@ from sphinx.util.matching import patfilter # noqa if False: # For type annotation - from typing import Any, Callable, Dict, IO, Iterable, Iterator, List, Pattern, Sequence, Set, Tuple, Type, Union # NOQA + from typing import Any, Callable, Dict, IO, Iterable, Iterator, List, Pattern, Set, Tuple, Type, Union # NOQA logger = logging.getLogger(__name__) @@ -138,7 +135,7 @@ class FilenameUniqDict(dict): while uniquename in self._existing: i += 1 uniquename = '%s%s%s' % (base, i, ext) - self[newfile] = (set([docname]), uniquename) + self[newfile] = ({docname}, uniquename) self._existing.add(uniquename) return uniquename @@ -196,36 +193,6 @@ class DownloadFiles(dict): self.add_file(docname, filename) -def copy_static_entry(source, targetdir, builder, context={}, - exclude_matchers=(), level=0): - # type: (str, str, Any, Dict, Tuple[Callable, ...], int) -> None - """[DEPRECATED] Copy a HTML builder static_path entry from source to targetdir. - - Handles all possible cases of files, directories and subdirectories. - """ - warnings.warn('sphinx.util.copy_static_entry is deprecated for removal', - RemovedInSphinx30Warning, stacklevel=2) - - if exclude_matchers: - relpath = relative_path(path.join(builder.srcdir, 'dummy'), source) - for matcher in exclude_matchers: - if matcher(relpath): - return - if path.isfile(source): - copy_asset_file(source, targetdir, context, builder.templates) - elif path.isdir(source): - ensuredir(targetdir) - for entry in os.listdir(source): - if entry.startswith('.'): - continue - newtarget = targetdir - if path.isdir(path.join(source, entry)): - newtarget = path.join(targetdir, entry) - copy_static_entry(path.join(source, entry), newtarget, - builder, context, level=level + 1, - exclude_matchers=exclude_matchers) - - _DEBUG_HEADER = '''\ # Sphinx version: %s # Python version: %s (%s) @@ -603,22 +570,26 @@ class PeekableIterator: def import_object(objname, source=None): # type: (str, str) -> Any + """Import python object by qualname.""" try: - module, name = objname.rsplit('.', 1) - except ValueError as err: - raise ExtensionError('Invalid full object name %s' % objname + - (source and ' (needed for %s)' % source or ''), - err) - try: - return getattr(__import__(module, None, None, [name]), name) - except ImportError as err: - raise ExtensionError('Could not import %s' % module + - (source and ' (needed for %s)' % source or ''), - err) - except AttributeError as err: - raise ExtensionError('Could not find %s' % objname + - (source and ' (needed for %s)' % source or ''), - err) + objpath = objname.split('.') + modname = objpath.pop(0) + obj = __import__(modname) + for name in objpath: + modname += '.' + name + try: + obj = getattr(obj, name) + except AttributeError: + __import__(modname) + obj = getattr(obj, name) + + return obj + except (AttributeError, ImportError) as exc: + if source: + raise ExtensionError('Could not import %s (needed for %s)' % + (objname, source), exc) + else: + raise ExtensionError('Could not import %s' % objname, exc) def encode_uri(uri): diff --git a/sphinx/util/compat.py b/sphinx/util/compat.py index 1567bad9e..805c17e5d 100644 --- a/sphinx/util/compat.py +++ b/sphinx/util/compat.py @@ -14,9 +14,8 @@ import warnings from docutils.utils import get_source_line from sphinx import addnodes -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.transforms import SphinxTransform -from sphinx.util import import_object if False: # For type annotation @@ -25,18 +24,6 @@ if False: from sphinx.config import Config # NOQA -def deprecate_source_parsers(app, config): - # type: (Sphinx, Config) -> None - if config.source_parsers: - warnings.warn('The config variable "source_parsers" is deprecated. ' - 'Please use app.add_source_parser() API instead.', - RemovedInSphinx30Warning) - for suffix, parser in config.source_parsers.items(): - if isinstance(parser, str): - parser = import_object(parser, 'source parser') - app.add_source_parser(suffix, parser) - - def register_application_for_autosummary(app): # type: (Sphinx) -> None """Register application object to autosummary module. @@ -58,18 +45,17 @@ class IndexEntriesMigrator(SphinxTransform): def apply(self, **kwargs): # type: (Any) -> None for node in self.document.traverse(addnodes.index): - for entries in node['entries']: + for i, entries in enumerate(node['entries']): if len(entries) == 4: source, line = get_source_line(node) warnings.warn('An old styled index node found: %r at (%s:%s)' % (node, source, line), RemovedInSphinx40Warning) - entries.extend([None]) + node['entries'][i] = entries + (None,) def setup(app): # type: (Sphinx) -> Dict[str, Any] app.add_transform(IndexEntriesMigrator) - app.connect('config-inited', deprecate_source_parsers) app.connect('builder-inited', register_application_for_autosummary) return { diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py index 66f074702..9b19d229d 100644 --- a/sphinx/util/docfields.py +++ b/sphinx/util/docfields.py @@ -9,16 +9,18 @@ :license: BSD, see LICENSE for details. """ +import warnings from typing import List, Tuple, cast from docutils import nodes from sphinx import addnodes +from sphinx.deprecation import RemovedInSphinx40Warning if False: # For type annotation from typing import Any, Dict, Type, Union # NOQA - from sphinx.domains import Domain # NOQA + from sphinx.directive import ObjectDescription # NOQA from sphinx.environment import BuildEnvironment # NOQA from sphinx.util.typing import TextlikeNode # NOQA @@ -245,15 +247,14 @@ class DocFieldTransformer: typemap = None # type: Dict[str, Tuple[Field, bool]] def __init__(self, directive): - # type: (Any) -> None + # type: (ObjectDescription) -> None self.directive = directive - if '_doc_field_type_map' not in directive.__class__.__dict__: - directive.__class__._doc_field_type_map = \ - self.preprocess_fieldtypes(directive.__class__.doc_field_types) - self.typemap = directive._doc_field_type_map + self.typemap = directive.get_field_type_map() def preprocess_fieldtypes(self, types): # type: (List[Field]) -> Dict[str, Tuple[Field, bool]] + warnings.warn('DocFieldTransformer.preprocess_fieldtypes() is deprecated.', + RemovedInSphinx40Warning) typemap = {} for fieldtype in types: for name in fieldtype.names: diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 393f97abf..d38c780f4 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -10,8 +10,6 @@ import os import re -import types -import warnings from contextlib import contextmanager from copy import copy from distutils.version import LooseVersion @@ -21,13 +19,11 @@ from typing import IO, cast import docutils from docutils import nodes from docutils.io import FileOutput -from docutils.parsers.rst import Directive, directives, roles, convert_directive_function +from docutils.parsers.rst import Directive, directives, roles from docutils.statemachine import StateMachine from docutils.utils import Reporter, unescape -from sphinx.deprecation import RemovedInSphinx30Warning -from sphinx.errors import ExtensionError, SphinxError -from sphinx.locale import __ +from sphinx.errors import SphinxError from sphinx.util import logging logger = logging.getLogger(__name__) @@ -42,7 +38,6 @@ if False: from sphinx.builders import Builder # NOQA from sphinx.config import Config # NOQA from sphinx.environment import BuildEnvironment # NOQA - from sphinx.io import SphinxFileInput # NOQA from sphinx.util.typing import RoleFunction # NOQA @@ -305,24 +300,6 @@ def is_html5_writer_available(): return __version_info__ > (0, 13, 0) -def directive_helper(obj, has_content=None, argument_spec=None, **option_spec): - # type: (Any, bool, Tuple[int, int, bool], Any) -> Any - warnings.warn('function based directive support is now deprecated. ' - 'Use class based directive instead.', - RemovedInSphinx30Warning) - - if isinstance(obj, (types.FunctionType, types.MethodType)): - obj.content = has_content # type: ignore - obj.arguments = argument_spec or (0, 0, False) # type: ignore - obj.options = option_spec # type: ignore - return convert_directive_function(obj) - else: - if has_content or argument_spec or option_spec: - raise ExtensionError(__('when adding directive classes, no ' - 'additional arguments may be given')) - return obj - - @contextmanager def switch_source_input(state, content): # type: (State, StringList) -> Generator[None, None, None] @@ -383,6 +360,11 @@ class SphinxDirective(Directive): """Reference to the :class:`.Config` object.""" return self.env.config + def set_source_info(self, node): + # type: (nodes.Node) -> None + """Set source and line number to the node.""" + node.source, node.line = self.state_machine.get_source_and_line(self.lineno) + class SphinxRole: """A base class for Sphinx roles. diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 9196e6fb6..89534f5ae 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -19,19 +19,19 @@ import babel.dates from babel.messages.mofile import write_mo from babel.messages.pofile import read_po -from sphinx.deprecation import RemovedInSphinx30Warning +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import SphinxError from sphinx.locale import __ from sphinx.util import logging from sphinx.util.matching import Matcher -from sphinx.util.osutil import SEP, relpath +from sphinx.util.osutil import SEP, canon_path, relpath logger = logging.getLogger(__name__) if False: # For type annotation - from typing import Callable, List, Set # NOQA + from typing import Callable, Generator, List, Set, Tuple # NOQA from sphinx.environment import BuildEnvironment # NOQA LocaleFileInfoBase = namedtuple('CatalogInfo', 'base_dir,domain,charset') @@ -81,8 +81,55 @@ class CatalogInfo(LocaleFileInfoBase): logger.warning(__('writing error: %s, %s'), self.mo_path, exc) +class CatalogRepository: + """A repository for message catalogs.""" + + def __init__(self, basedir, locale_dirs, language, encoding): + # type: (str, List[str], str, str) -> None + self.basedir = basedir + self._locale_dirs = locale_dirs + self.language = language + self.encoding = encoding + + @property + def locale_dirs(self): + # type: () -> Generator[str, None, None] + if not self.language: + return + + for locale_dir in self._locale_dirs: + locale_dir = path.join(self.basedir, locale_dir) + if path.exists(path.join(locale_dir, self.language, 'LC_MESSAGES')): + yield locale_dir + + @property + def pofiles(self): + # type: () -> Generator[Tuple[str, str], None, None] + for locale_dir in self.locale_dirs: + basedir = path.join(locale_dir, self.language, 'LC_MESSAGES') + for root, dirnames, filenames in os.walk(basedir): + # skip dot-directories + for dirname in dirnames: + if dirname.startswith('.'): + dirnames.remove(dirname) + + for filename in filenames: + if filename.endswith('.po'): + fullpath = path.join(root, filename) + yield basedir, relpath(fullpath, basedir) + + @property + def catalogs(self): + # type: () -> Generator[CatalogInfo, None, None] + for basedir, filename in self.pofiles: + domain = canon_path(path.splitext(filename)[0]) + yield CatalogInfo(basedir, domain, self.encoding) + + def find_catalog(docname, compaction): # type: (str, bool) -> str + warnings.warn('find_catalog() is deprecated.', + RemovedInSphinx40Warning, stacklevel=2) if compaction: ret = docname.split(SEP, 1)[0] else: @@ -91,8 +138,19 @@ def find_catalog(docname, compaction): return ret +def docname_to_domain(docname, compation): + # type: (str, bool) -> str + """Convert docname to domain for catalogs.""" + if compation: + return docname.split(SEP, 1)[0] + else: + return docname + + def find_catalog_files(docname, srcdir, locale_dirs, lang, compaction): # type: (str, str, List[str], str, bool) -> List[str] + warnings.warn('find_catalog_files() is deprecated.', + RemovedInSphinx40Warning, stacklevel=2) if not(lang and locale_dirs): return [] @@ -103,10 +161,9 @@ def find_catalog_files(docname, srcdir, locale_dirs, lang, compaction): return files -def find_catalog_source_files(locale_dirs, locale, domains=None, gettext_compact=None, - charset='utf-8', force_all=False, - excluded=Matcher([])): - # type: (List[str], str, List[str], bool, str, bool, Matcher) -> Set[CatalogInfo] +def find_catalog_source_files(locale_dirs, locale, domains=None, charset='utf-8', + force_all=False, excluded=Matcher([])): + # type: (List[str], str, List[str], str, bool, Matcher) -> Set[CatalogInfo] """ :param list locale_dirs: list of path as `['locale_dir1', 'locale_dir2', ...]` to find @@ -120,9 +177,8 @@ def find_catalog_source_files(locale_dirs, locale, domains=None, gettext_compact default is False. :return: [CatalogInfo(), ...] """ - if gettext_compact is not None: - warnings.warn('gettext_compact argument for find_catalog_source_files() ' - 'is deprecated.', RemovedInSphinx30Warning, stacklevel=2) + warnings.warn('find_catalog_source_files() is deprecated.', + RemovedInSphinx40Warning, stacklevel=2) catalogs = set() # type: Set[CatalogInfo] diff --git a/sphinx/util/images.py b/sphinx/util/images.py index 72f976740..76bffd88c 100644 --- a/sphinx/util/images.py +++ b/sphinx/util/images.py @@ -10,16 +10,12 @@ import base64 import imghdr -import warnings from collections import OrderedDict -from io import BytesIO from os import path from typing import NamedTuple import imagesize -from sphinx.deprecation import RemovedInSphinx30Warning - try: from PIL import Image except ImportError: @@ -27,7 +23,7 @@ except ImportError: if False: # For type annotation - from typing import Dict, IO, List, Tuple # NOQA + from typing import IO, Tuple # NOQA mime_suffixes = OrderedDict([ ('.gif', 'image/gif'), @@ -72,17 +68,11 @@ def guess_mimetype_for_stream(stream, default=None): return default -def guess_mimetype(filename='', content=None, default=None): - # type: (str, bytes, str) -> str +def guess_mimetype(filename, default=None): + # type: (str, str) -> str _, ext = path.splitext(filename.lower()) if ext in mime_suffixes: return mime_suffixes[ext] - elif content: - # TODO: When the content argument is removed, make filename a required - # argument. - warnings.warn('The content argument of guess_mimetype() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return guess_mimetype_for_stream(BytesIO(content), default=default) elif path.exists(filename): with open(filename, 'rb') as f: return guess_mimetype_for_stream(f, default=default) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 89dd842e3..ff399c439 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -14,11 +14,9 @@ import inspect import re import sys import typing -import warnings from functools import partial from io import StringIO -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.util import logging from sphinx.util.typing import NoneType @@ -264,26 +262,6 @@ def is_builtin_class_method(obj, attr_name): return getattr(builtins, safe_getattr(cls, '__name__', '')) is cls # type: ignore -class Parameter: - """Fake parameter class for python2.""" - POSITIONAL_ONLY = 0 - POSITIONAL_OR_KEYWORD = 1 - VAR_POSITIONAL = 2 - KEYWORD_ONLY = 3 - VAR_KEYWORD = 4 - empty = object() - - def __init__(self, name, kind=POSITIONAL_OR_KEYWORD, default=empty): - # type: (str, int, Any) -> None - self.name = name - self.kind = kind - self.default = default - self.annotation = self.empty - - warnings.warn('sphinx.util.inspect.Parameter is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - - class Signature: """The Signature object represents the call signature of a callable object and its return annotation. diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py index 786898181..0fdc3e833 100644 --- a/sphinx/util/inventory.py +++ b/sphinx/util/inventory.py @@ -15,7 +15,7 @@ from sphinx.util import logging if False: # For type annotation - from typing import Callable, Dict, IO, Iterator, Tuple # NOQA + from typing import Callable, IO, Iterator # NOQA from sphinx.builders import Builder # NOQA from sphinx.environment import BuildEnvironment # NOQA from sphinx.util.typing import Inventory # NOQA diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index ba71ecbe1..afa4ebd23 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -22,7 +22,6 @@ from sphinx.util.console import colorize if False: # For type annotation from typing import Any, Dict, Generator, IO, List, Tuple, Type, Union # NOQA - from docutils import nodes # NOQA from sphinx.application import Sphinx # NOQA diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index b0dd13b38..60e0144b0 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -9,17 +9,19 @@ """ import re +import warnings from typing import Any, cast from docutils import nodes from sphinx import addnodes +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import __ from sphinx.util import logging if False: # For type annotation - from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Type # NOQA + from typing import Callable, Iterable, List, Optional, Set, Tuple, Type # NOQA from docutils.parsers.rst.states import Inliner # NOQA from docutils.statemachine import StringList # NOQA from sphinx.builders import Builder # NOQA @@ -152,7 +154,7 @@ def apply_source_workaround(node): # workaround: literal_block under bullet list (#4913) if isinstance(node, nodes.literal_block) and node.source is None: - node.source = find_source_node(node) + node.source = get_node_source(node) # workaround: recommonmark-0.2.0 doesn't set rawsource attribute if not node.rawsource: @@ -170,7 +172,7 @@ def apply_source_workaround(node): ))): logger.debug('[i18n] PATCH: %r to have source and line: %s', get_full_module_name(node), repr_domxml(node)) - node.source = find_source_node(node) + node.source = get_node_source(node) node.line = 0 # need fix docutils to get `node.line` return @@ -278,6 +280,13 @@ def extract_messages(doctree): def find_source_node(node): + # type: (nodes.Element) -> str + warnings.warn('find_source_node() is deprecated.', + RemovedInSphinx40Warning) + return get_node_source(node) + + +def get_node_source(node): # type: (nodes.Element) -> str for pnode in traverse_parent(node): if pnode.source: @@ -467,6 +476,12 @@ def set_role_source_info(inliner, lineno, node): node.source, node.line = inliner.reporter.get_source_and_line(lineno) # type: ignore +def copy_source_info(src, dst): + # type: (nodes.Element, nodes.Element) -> None + dst.source = get_node_source(src) + dst.line = get_node_line(src) + + NON_SMARTQUOTABLE_PARENT_NODES = ( nodes.FixedTextElement, nodes.literal, @@ -513,6 +528,7 @@ def process_only_nodes(document, tags): # monkey-patch Element.copy to copy the rawsource and line +# for docutils-0.14 or older versions. def _new_copy(self): # type: (nodes.Element) -> nodes.Element diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index 646a92f1b..4cd646f96 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -15,16 +15,15 @@ import os import re import shutil import sys -import time import warnings from io import StringIO from os import path -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning if False: # For type annotation - from typing import Any, Iterator, List, Tuple, Union # NOQA + from typing import Any, Iterator, List, Tuple # NOQA # Errnos that we need. EEXIST = getattr(errno, 'EEXIST', 0) # RemovedInSphinx40Warning @@ -148,28 +147,6 @@ def make_filename_from_project(project): return make_filename(project_suffix_re.sub('', project)).lower() -def ustrftime(format, *args): - # type: (str, Any) -> str - """[DEPRECATED] strftime for unicode strings.""" - warnings.warn('sphinx.util.osutil.ustrtime is deprecated for removal', - RemovedInSphinx30Warning, stacklevel=2) - - if not args: - # If time is not specified, try to use $SOURCE_DATE_EPOCH variable - # See https://wiki.debian.org/ReproducibleBuilds/TimestampsProposal - source_date_epoch = os.getenv('SOURCE_DATE_EPOCH') - if source_date_epoch is not None: - time_struct = time.gmtime(float(source_date_epoch)) - args = [time_struct] # type: ignore - # On Windows, time.strftime() and Unicode characters will raise UnicodeEncodeError. - # https://bugs.python.org/issue8304 - try: - return time.strftime(format, *args) - except UnicodeEncodeError: - r = time.strftime(format.encode('unicode-escape').decode(), *args) - return r.encode().decode('unicode-escape') - - def relpath(path, start=os.curdir): # type: (str, str) -> str """Return a relative filepath to *path* either from the current directory or diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index fe5f0803e..dca2849c2 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -22,7 +22,7 @@ from sphinx.util.typing import NoneType if False: # For type annotation - from typing import Any, Callable, Generator # NOQA + from typing import Any, Callable # NOQA logger = logging.getLogger(__name__) diff --git a/sphinx/versioning.py b/sphinx/versioning.py index d69720c57..56c126da2 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -9,19 +9,18 @@ :license: BSD, see LICENSE for details. """ import pickle -import warnings from itertools import product, zip_longest from operator import itemgetter from os import path from uuid import uuid4 -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.transforms import SphinxTransform if False: # For type annotation - from typing import Any, Iterator # NOQA + from typing import Any, Dict, Iterator # NOQA from docutils import nodes # NOQA + from sphinx.application import Sphinx # NOQA try: import Levenshtein @@ -179,10 +178,12 @@ class UIDTransform(SphinxTransform): list(merge_doctrees(old_doctree, self.document, env.versioning_condition)) -def prepare(document): - # type: (nodes.document) -> None - """Simple wrapper for UIDTransform.""" - warnings.warn('versioning.prepare() is deprecated. Use UIDTransform instead.', - RemovedInSphinx30Warning, stacklevel=2) - transform = UIDTransform(document) - transform.apply() +def setup(app): + # type: (Sphinx) -> Dict[str, Any] + app.add_transform(UIDTransform) + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 454e7c4d2..60cc7d999 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -11,7 +11,6 @@ import copy import os import posixpath -import sys import warnings from typing import Iterable, cast @@ -20,7 +19,7 @@ from docutils.writers.html4css1 import Writer, HTMLTranslator as BaseTranslator from sphinx import addnodes from sphinx.builders import Builder -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import admonitionlabels, _, __ from sphinx.util import logging from sphinx.util.docutils import SphinxTranslator @@ -509,9 +508,7 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'): self.add_permalink_ref(node.parent, _('Permalink to this code')) elif isinstance(node.parent, nodes.figure): - image_nodes = node.parent.traverse(nodes.image) - target_node = image_nodes and image_nodes[0] or node.parent - self.add_permalink_ref(target_node, _('Permalink to this image')) + self.add_permalink_ref(node.parent, _('Permalink to this image')) elif node.parent.get('toctree'): self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree')) @@ -947,33 +944,3 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): def unknown_visit(self, node): # type: (nodes.Node) -> None raise NotImplementedError('Unknown node: ' + node.__class__.__name__) - - # --------- METHODS FOR COMPATIBILITY -------------------------------------- - - @property - def highlightlang(self): - # type: () -> str - warnings.warn('HTMLTranslator.highlightlang is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_language - - @property - def highlightlang_base(self): - # type: () -> str - warnings.warn('HTMLTranslator.highlightlang_base is deprecated.', - RemovedInSphinx30Warning) - return self.builder.config.highlight_language - - @property - def highlightopts(self): - # type: () -> str - warnings.warn('HTMLTranslator.highlightopts is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_options - - @property - def highlightlinenothreshold(self): - # type: () -> int - warnings.warn('HTMLTranslator.highlightlinenothreshold is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return sys.maxsize diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index 5a10c990f..afab35950 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -10,7 +10,6 @@ import os import posixpath -import sys import warnings from typing import Iterable, cast @@ -19,7 +18,7 @@ from docutils.writers.html5_polyglot import HTMLTranslator as BaseTranslator from sphinx import addnodes from sphinx.builders import Builder -from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.locale import admonitionlabels, _, __ from sphinx.util import logging from sphinx.util.docutils import SphinxTranslator @@ -455,9 +454,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'): self.add_permalink_ref(node.parent, _('Permalink to this code')) elif isinstance(node.parent, nodes.figure): - image_nodes = node.parent.traverse(nodes.image) - target_node = image_nodes and image_nodes[0] or node.parent - self.add_permalink_ref(target_node, _('Permalink to this image')) + self.add_permalink_ref(node.parent, _('Permalink to this image')) elif node.parent.get('toctree'): self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree')) @@ -884,33 +881,3 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): def unknown_visit(self, node): # type: (nodes.Node) -> None raise NotImplementedError('Unknown node: ' + node.__class__.__name__) - - # --------- METHODS FOR COMPATIBILITY -------------------------------------- - - @property - def highlightlang(self): - # type: () -> str - warnings.warn('HTMLTranslator.highlightlang is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_language - - @property - def highlightlang_base(self): - # type: () -> str - warnings.warn('HTMLTranslator.highlightlang_base is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_language - - @property - def highlightopts(self): - # type: () -> str - warnings.warn('HTMLTranslator.highlightopts is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return self.builder.config.highlight_options - - @property - def highlightlinenothreshold(self): - # type: () -> int - warnings.warn('HTMLTranslator.highlightlinenothreshold is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return sys.maxsize diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 9ee51f8aa..b45a95eef 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -12,7 +12,6 @@ """ import re -import sys import warnings from collections import defaultdict from os import path @@ -22,9 +21,7 @@ from docutils import nodes, writers from sphinx import addnodes from sphinx import highlighting -from sphinx.deprecation import ( - RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias -) +from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.domains.std import StandardDomain from sphinx.errors import SphinxError from sphinx.locale import admonitionlabels, _, __ @@ -284,20 +281,6 @@ class Table: # (cell = rectangular area) self.cell_id = 0 # last assigned cell_id - @property - def caption_footnotetexts(self): - # type: () -> List[str] - warnings.warn('table.caption_footnotetexts is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return [] - - @property - def header_footnotetexts(self): - # type: () -> List[str] - warnings.warn('table.header_footnotetexts is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return [] - def is_longtable(self): # type: () -> bool """True if and only if table uses longtable environment.""" @@ -653,27 +636,6 @@ class LaTeXTranslator(SphinxTranslator): self.body = self.bodystack.pop() return body - def restrict_footnote(self, node): - # type: (nodes.Element) -> None - warnings.warn('LaTeXWriter.restrict_footnote() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - - if self.footnote_restricted is None: - self.footnote_restricted = node - self.pending_footnotes = [] - - def unrestrict_footnote(self, node): - # type: (nodes.Element) -> None - warnings.warn('LaTeXWriter.unrestrict_footnote() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - - if self.footnote_restricted == node: - self.footnote_restricted = None - for footnode in self.pending_footnotes: - footnode['footnotetext'] = True - footnode.walkabout(self) - self.pending_footnotes = [] - def format_docclass(self, docclass): # type: (str) -> str """ prepends prefix to sphinx document classes @@ -1533,7 +1495,11 @@ class LaTeXTranslator(SphinxTranslator): # in reverse order post = [] # type: List[str] include_graphics_options = [] - is_inline = self.is_inline(node) + has_hyperlink = isinstance(node.parent, nodes.reference) + if has_hyperlink: + is_inline = self.is_inline(node.parent) + else: + is_inline = self.is_inline(node) if 'width' in attrs: if 'scale' in attrs: w = self.latex_image_length(attrs['width'], attrs['scale']) @@ -1575,7 +1541,7 @@ class LaTeXTranslator(SphinxTranslator): if self.in_parsed_literal: pre.append('{\\sphinxunactivateextrasandspace ') post.append('}') - if not is_inline: + if not is_inline and not has_hyperlink: pre.append('\n\\noindent') post.append('\n') pre.reverse() @@ -1777,8 +1743,8 @@ class LaTeXTranslator(SphinxTranslator): # type: (nodes.Element) -> None self.body.append('\n\\end{flushright}\n') - def visit_index(self, node, scre = None): - # type: (nodes.Element, Pattern) -> None + def visit_index(self, node): + # type: (nodes.Element) -> None def escape(value): value = self.encode(value) value = value.replace(r'\{', r'\sphinxleftcurlybrace{}') @@ -1795,10 +1761,6 @@ class LaTeXTranslator(SphinxTranslator): else: return '\\spxentry{%s}' % string - if scre: - warnings.warn(('LaTeXTranslator.visit_index() optional argument ' - '"scre" is deprecated. It is ignored.'), - RemovedInSphinx30Warning, stacklevel=2) if not node.get('inline', True): self.body.append('\n') entries = node['entries'] @@ -1863,6 +1825,8 @@ class LaTeXTranslator(SphinxTranslator): for id in node.get('ids'): anchor = not self.in_caption self.body += self.hypertarget(id, anchor=anchor) + if not self.is_inline(node): + self.body.append('\n') uri = node.get('refuri', '') if not uri and node.get('refid'): uri = '%' + self.curfilestack[-1] + '#' + node['refid'] @@ -1916,6 +1880,8 @@ class LaTeXTranslator(SphinxTranslator): def depart_reference(self, node): # type: (nodes.Element) -> None self.body.append(self.context.pop()) + if not self.is_inline(node): + self.body.append('\n') def visit_number_reference(self, node): # type: (nodes.Element) -> None @@ -2488,70 +2454,6 @@ class LaTeXTranslator(SphinxTranslator): fnotes[num] = [newnode, False] return fnotes - @property - def footnotestack(self): - # type: () -> List[Dict[str, List[Union[collected_footnote, bool]]]] - warnings.warn('LaTeXWriter.footnotestack is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return [] - - @property - def bibitems(self): - # type: () -> List[List[str]] - warnings.warn('LaTeXTranslator.bibitems() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return [] - - @property - def in_container_literal_block(self): - # type: () -> int - warnings.warn('LaTeXTranslator.in_container_literal_block is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return 0 - - @property - def next_section_ids(self): - # type: () -> Set[str] - warnings.warn('LaTeXTranslator.next_section_ids is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return set() - - @property - def next_hyperlink_ids(self): - # type: () -> Dict - warnings.warn('LaTeXTranslator.next_hyperlink_ids is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return {} - - def push_hyperlink_ids(self, figtype, ids): - # type: (str, Set[str]) -> None - warnings.warn('LaTeXTranslator.push_hyperlink_ids() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - pass - - def pop_hyperlink_ids(self, figtype): - # type: (str) -> Set[str] - warnings.warn('LaTeXTranslator.pop_hyperlink_ids() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return set() - - @property - def hlsettingstack(self): - # type: () -> List[List[Union[str, int]]] - warnings.warn('LaTeXTranslator.hlsettingstack is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - return [[self.builder.config.highlight_language, sys.maxsize]] - - def check_latex_elements(self): - # type: () -> None - warnings.warn('check_latex_elements() is deprecated.', - RemovedInSphinx30Warning, stacklevel=2) - - for key in self.builder.config.latex_elements: - if key not in self.elements: - msg = __("Unknown configure key: latex_elements[%r] is ignored.") - logger.warning(msg % key) - def babel_defmacro(self, name, definition): # type: (str, str) -> str warnings.warn('babel_defmacro() is deprecated.', @@ -2566,17 +2468,6 @@ class LaTeXTranslator(SphinxTranslator): return ('%s\\def%s{%s}%s\n' % (prefix, name, definition, suffix)) - def _make_visit_admonition(name): # type: ignore - # type: (str) -> Callable[[LaTeXTranslator, nodes.Element], None] - warnings.warn('LaTeXTranslator._make_visit_admonition() is deprecated.', - RemovedInSphinx30Warning) - - def visit_admonition(self, node): - # type: (nodes.Element) -> None - self.body.append('\n\\begin{sphinxadmonition}{%s}{%s:}' % - (name, admonitionlabels[name])) - return visit_admonition - def generate_numfig_format(self, builder): # type: (LaTeXBuilder) -> str warnings.warn('generate_numfig_format() is deprecated.', @@ -2619,16 +2510,9 @@ class LaTeXTranslator(SphinxTranslator): # Import old modules here for compatibility -from sphinx.builders.latex.transforms import URI_SCHEMES, ShowUrlsTransform # NOQA from sphinx.builders.latex.util import ExtBabel # NOQA -deprecated_alias('sphinx.writers.latex', - { - 'ShowUrlsTransform': ShowUrlsTransform, - 'URI_SCHEMES': URI_SCHEMES, - }, - RemovedInSphinx30Warning) deprecated_alias('sphinx.writers.latex', { 'ExtBabel': ExtBabel, diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index f743f64f1..0856ee5ee 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -467,6 +467,21 @@ class ManualPageTranslator(SphinxTranslator, BaseTranslator): # type: (nodes.Element) -> None return self.depart_strong(node) + # overwritten: handle section titles better than in 0.6 release + def visit_caption(self, node): + # type: (nodes.Element) -> None + if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'): + self.body.append('.sp\n') + else: + super().visit_caption(node) + + def depart_caption(self, node): + # type: (nodes.Element) -> None + if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'): + self.body.append('\n') + else: + super().depart_caption(node) + # overwritten: handle section titles better than in 0.6 release def visit_title(self, node): # type: (nodes.Element) -> None diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index ede48ec60..77d3dcb69 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -10,14 +10,12 @@ import re import textwrap -import warnings from os import path from typing import Iterable, cast from docutils import nodes, writers from sphinx import addnodes, __display_version__ -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.errors import ExtensionError from sphinx.locale import admonitionlabels, _, __ from sphinx.util import logging @@ -1355,8 +1353,9 @@ class TexinfoTranslator(SphinxTranslator): width = self.tex_image_length(attrs.get('width', '')) height = self.tex_image_length(attrs.get('height', '')) alt = self.escape_arg(attrs.get('alt', '')) + filename = "%s-figures/%s" % (self.elements['filename'][:-5], name) # type: ignore self.body.append('\n@image{%s,%s,%s,%s,%s}\n' % - (name, width, height, alt, ext[1:])) + (filename, width, height, alt, ext[1:])) def depart_image(self, node): # type: (nodes.Element) -> None @@ -1744,13 +1743,3 @@ class TexinfoTranslator(SphinxTranslator): self.body.append('\n\n@example\n%s\n@end example\n\n' % self.escape_arg(node.astext())) raise nodes.SkipNode - - def _make_visit_admonition(name): # type: ignore - # type: (str) -> Callable[[TexinfoTranslator, nodes.Element], None] - warnings.warn('TexinfoTranslator._make_visit_admonition() is deprecated.', - RemovedInSphinx30Warning) - - def visit(self, node): - # type: (nodes.Element) -> None - self.visit_admonition(node, admonitionlabels[name]) - return visit diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 1447510c3..47ab9f2e4 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -11,7 +11,6 @@ import math import os import re import textwrap -import warnings from itertools import groupby, chain from typing import Iterable, cast @@ -19,7 +18,6 @@ from docutils import nodes, writers from docutils.utils import column_width from sphinx import addnodes -from sphinx.deprecation import RemovedInSphinx30Warning from sphinx.locale import admonitionlabels, _ from sphinx.util.docutils import SphinxTranslator @@ -1368,13 +1366,3 @@ class TextTranslator(SphinxTranslator): def unknown_visit(self, node): # type: (nodes.Node) -> None raise NotImplementedError('Unknown node: ' + node.__class__.__name__) - - def _make_depart_admonition(name): # type: ignore - # type: (str) -> Callable[[TextTranslator, nodes.Element], None] - warnings.warn('TextTranslator._make_depart_admonition() is deprecated.', - RemovedInSphinx30Warning) - - def depart_admonition(self, node): - # type: (nodes.Element) -> None - self.end_state(first=admonitionlabels[name] + ': ') - return depart_admonition diff --git a/sphinx/writers/xml.py b/sphinx/writers/xml.py index 9560c4006..8f6d393e2 100644 --- a/sphinx/writers/xml.py +++ b/sphinx/writers/xml.py @@ -12,7 +12,7 @@ from docutils.writers.docutils_xml import Writer as BaseXMLWriter if False: # For type annotation - from typing import Any, Tuple # NOQA + from typing import Any # NOQA from sphinx.builders import Builder # NOQA diff --git a/tests/roots/test-add_source_parser/conf.py b/tests/roots/test-add_source_parser/conf.py index 9ff50b21e..2acd4d24c 100644 --- a/tests/roots/test-add_source_parser/conf.py +++ b/tests/roots/test-add_source_parser/conf.py @@ -1,17 +1,8 @@ import os import sys -from docutils.parsers import Parser - sys.path.insert(0, os.path.abspath('.')) -class DummyMarkdownParser(Parser): - supported = ('markdown',) - - extensions = ['source_parser'] -source_suffix = ['.rst', '.md'] -source_parsers = { - '.md': DummyMarkdownParser -} +source_suffix = ['.rst'] diff --git a/tests/roots/test-builder-gettext-dont-rebuild-mo/conf.py b/tests/roots/test-builder-gettext-dont-rebuild-mo/conf.py index e69de29bb..d13f727e7 100644 --- a/tests/roots/test-builder-gettext-dont-rebuild-mo/conf.py +++ b/tests/roots/test-builder-gettext-dont-rebuild-mo/conf.py @@ -0,0 +1 @@ +language = 'xx' diff --git a/tests/roots/test-builder-gettext-dont-rebuild-mo/bom.po b/tests/roots/test-builder-gettext-dont-rebuild-mo/xx/LC_MESSAGES/bom.po similarity index 100% rename from tests/roots/test-builder-gettext-dont-rebuild-mo/bom.po rename to tests/roots/test-builder-gettext-dont-rebuild-mo/xx/LC_MESSAGES/bom.po diff --git a/tests/roots/test-changes/base.rst b/tests/roots/test-changes/base.rst new file mode 100644 index 000000000..a1b28398a --- /dev/null +++ b/tests/roots/test-changes/base.rst @@ -0,0 +1,20 @@ +Version markup +-------------- + +.. versionadded:: 0.6 + Some funny **stuff**. + +.. versionchanged:: 0.6 + Even more funny stuff. + +.. deprecated:: 0.6 + Boring stuff. + +.. versionadded:: 1.2 + + First paragraph of versionadded. + +.. versionchanged:: 1.2 + First paragraph of versionchanged. + + Second paragraph of versionchanged. diff --git a/tests/roots/test-changes/c-api.rst b/tests/roots/test-changes/c-api.rst new file mode 100644 index 000000000..f0ad413cd --- /dev/null +++ b/tests/roots/test-changes/c-api.rst @@ -0,0 +1,24 @@ +.. highlight:: c + + +Memory +====== + +.. c:function:: void* Test_Malloc(size_t n) + + Allocate *n* bytes of memory. + + .. versionchanged:: 0.6 + + Can now be replaced with a different allocator. + +System +------ + +Access to the system allocator. + +.. versionadded:: 0.6 + +.. c:function:: void* Test_SysMalloc(size_t n) + + Allocate *n* bytes of memory using system allocator. diff --git a/tests/roots/test-changes/conf.py b/tests/roots/test-changes/conf.py new file mode 100644 index 000000000..94cb629dc --- /dev/null +++ b/tests/roots/test-changes/conf.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +project = 'Sphinx ChangesBuilder tests' +copyright = '2007-2019 by the Sphinx team, see AUTHORS' +version = '0.6' +release = '0.6alpha1' diff --git a/tests/roots/test-changes/contents.rst b/tests/roots/test-changes/contents.rst new file mode 100644 index 000000000..ced802608 --- /dev/null +++ b/tests/roots/test-changes/contents.rst @@ -0,0 +1,13 @@ +Index for ChangesBuilder tests +============================== + +Contents: + +.. toctree:: + :maxdepth: 2 + :caption: Table of Contents + :name: mastertoc + + base + c-api + library/utils diff --git a/tests/roots/test-changes/library/utils.rst b/tests/roots/test-changes/library/utils.rst new file mode 100644 index 000000000..86446995b --- /dev/null +++ b/tests/roots/test-changes/library/utils.rst @@ -0,0 +1,25 @@ +:mod:`utils` --- Fake utilities module for tests +================================================ + +.. module:: utils + :synopsis: Utility functions + +-------------- + +The :mod:`utils` module is a pretend python module for changes testing. + + +Classes +------- + +.. class:: Path + + Class for handling paths. + + .. versionadded:: 0.5 + + Innovative new way to handle paths. + + .. deprecated:: 0.6 + + So, that was a bad idea it turns out. diff --git a/tests/roots/test-directive-code/linenothreshold.rst b/tests/roots/test-directive-code/linenothreshold.rst new file mode 100644 index 000000000..09ee67efc --- /dev/null +++ b/tests/roots/test-directive-code/linenothreshold.rst @@ -0,0 +1,23 @@ +Code Blocks and Literal Includes with Line Numbers via linenothreshold +====================================================================== + +.. highlight:: python + :linenothreshold: 5 + +.. code-block:: + + class Foo: + pass + + class Bar: + def baz(): + pass + +.. code-block:: + + # comment + value = True + +.. literalinclude:: literal.inc + +.. literalinclude:: literal-short.inc diff --git a/tests/roots/test-directive-code/literal-short.inc b/tests/roots/test-directive-code/literal-short.inc new file mode 100644 index 000000000..7a07a3f7a --- /dev/null +++ b/tests/roots/test-directive-code/literal-short.inc @@ -0,0 +1,3 @@ +# Very small literal include (linenothreshold check) + +value = True diff --git a/tests/roots/test-ext-math-compat/conf.py b/tests/roots/test-ext-math-compat/conf.py index 30d26f97c..85e3950a5 100644 --- a/tests/roots/test-ext-math-compat/conf.py +++ b/tests/roots/test-ext-math-compat/conf.py @@ -1,17 +1,18 @@ +from docutils import nodes from docutils.parsers.rst import Directive -from sphinx.ext.mathbase import math, displaymath - extensions = ['sphinx.ext.mathjax'] def my_math_role(role, rawtext, text, lineno, inliner, options={}, content=[]): - return [math(latex='E = mc^2')], [] + text = 'E = mc^2' + return [nodes.math(text, text)], [] class MyMathDirective(Directive): def run(self): - return [displaymath(latex='E = mc^2')] + text = 'E = mc^2' + return [nodes.math_block(text, text)] def setup(app): diff --git a/tests/roots/test-images/index.rst b/tests/roots/test-images/index.rst index d1478fab1..67b742b27 100644 --- a/tests/roots/test-images/index.rst +++ b/tests/roots/test-images/index.rst @@ -15,6 +15,13 @@ test-image .. image:: testimäge.png +.. image:: rimg.png + :target: https://www.sphinx-doc.org/ + +.. image:: rimg.png + :align: center + :target: https://www.python.org/ + .. a remote image .. image:: https://www.python.org/static/img/python-logo.png diff --git a/tests/roots/test-index_on_title/conf.py b/tests/roots/test-index_on_title/conf.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/roots/test-index_on_title/contents.rst b/tests/roots/test-index_on_title/contents.rst new file mode 100644 index 000000000..8256c42d7 --- /dev/null +++ b/tests/roots/test-index_on_title/contents.rst @@ -0,0 +1,5 @@ +index_on_title +============== + +Test for :index:`index` in top level title +------------------------------------------ diff --git a/tests/roots/test-intl/index.txt b/tests/roots/test-intl/index.txt index cd63b5ec3..1e09294f9 100644 --- a/tests/roots/test-intl/index.txt +++ b/tests/roots/test-intl/index.txt @@ -30,3 +30,10 @@ CONTENTS refs section topic + +.. toctree:: + :maxdepth: 2 + :caption: Hidden Toc + :hidden: + + only diff --git a/tests/roots/test-intl/admonitions.po b/tests/roots/test-intl/xx/LC_MESSAGES/admonitions.po similarity index 100% rename from tests/roots/test-intl/admonitions.po rename to tests/roots/test-intl/xx/LC_MESSAGES/admonitions.po diff --git a/tests/roots/test-intl/bom.po b/tests/roots/test-intl/xx/LC_MESSAGES/bom.po similarity index 100% rename from tests/roots/test-intl/bom.po rename to tests/roots/test-intl/xx/LC_MESSAGES/bom.po diff --git a/tests/roots/test-intl/definition_terms.po b/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po similarity index 100% rename from tests/roots/test-intl/definition_terms.po rename to tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po diff --git a/tests/roots/test-intl/docfields.po b/tests/roots/test-intl/xx/LC_MESSAGES/docfields.po similarity index 100% rename from tests/roots/test-intl/docfields.po rename to tests/roots/test-intl/xx/LC_MESSAGES/docfields.po diff --git a/tests/roots/test-intl/external_links.po b/tests/roots/test-intl/xx/LC_MESSAGES/external_links.po similarity index 100% rename from tests/roots/test-intl/external_links.po rename to tests/roots/test-intl/xx/LC_MESSAGES/external_links.po diff --git a/tests/roots/test-intl/figure.po b/tests/roots/test-intl/xx/LC_MESSAGES/figure.po similarity index 100% rename from tests/roots/test-intl/figure.po rename to tests/roots/test-intl/xx/LC_MESSAGES/figure.po diff --git a/tests/roots/test-intl/footnote.po b/tests/roots/test-intl/xx/LC_MESSAGES/footnote.po similarity index 100% rename from tests/roots/test-intl/footnote.po rename to tests/roots/test-intl/xx/LC_MESSAGES/footnote.po diff --git a/tests/roots/test-intl/glossary_terms.po b/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms.po similarity index 100% rename from tests/roots/test-intl/glossary_terms.po rename to tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms.po diff --git a/tests/roots/test-intl/glossary_terms_inconsistency.po b/tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po similarity index 100% rename from tests/roots/test-intl/glossary_terms_inconsistency.po rename to tests/roots/test-intl/xx/LC_MESSAGES/glossary_terms_inconsistency.po diff --git a/tests/roots/test-intl/index.po b/tests/roots/test-intl/xx/LC_MESSAGES/index.po similarity index 94% rename from tests/roots/test-intl/index.po rename to tests/roots/test-intl/xx/LC_MESSAGES/index.po index 76ef049f0..a4646f1ec 100644 --- a/tests/roots/test-intl/index.po +++ b/tests/roots/test-intl/xx/LC_MESSAGES/index.po @@ -19,6 +19,9 @@ msgstr "" msgid "Table of Contents" msgstr "TABLE OF CONTENTS" +msgid "Hidden Toc" +msgstr "HIDDEN TOC" + msgid "testdata for i18n" msgstr "TESTDATA FOR I18N" diff --git a/tests/roots/test-intl/index_entries.po b/tests/roots/test-intl/xx/LC_MESSAGES/index_entries.po similarity index 100% rename from tests/roots/test-intl/index_entries.po rename to tests/roots/test-intl/xx/LC_MESSAGES/index_entries.po diff --git a/tests/roots/test-intl/label_target.po b/tests/roots/test-intl/xx/LC_MESSAGES/label_target.po similarity index 100% rename from tests/roots/test-intl/label_target.po rename to tests/roots/test-intl/xx/LC_MESSAGES/label_target.po diff --git a/tests/roots/test-intl/literalblock.po b/tests/roots/test-intl/xx/LC_MESSAGES/literalblock.po similarity index 100% rename from tests/roots/test-intl/literalblock.po rename to tests/roots/test-intl/xx/LC_MESSAGES/literalblock.po diff --git a/tests/roots/test-intl/only.po b/tests/roots/test-intl/xx/LC_MESSAGES/only.po similarity index 100% rename from tests/roots/test-intl/only.po rename to tests/roots/test-intl/xx/LC_MESSAGES/only.po diff --git a/tests/roots/test-intl/raw.po b/tests/roots/test-intl/xx/LC_MESSAGES/raw.po similarity index 100% rename from tests/roots/test-intl/raw.po rename to tests/roots/test-intl/xx/LC_MESSAGES/raw.po diff --git a/tests/roots/test-intl/refs.po b/tests/roots/test-intl/xx/LC_MESSAGES/refs.po similarity index 100% rename from tests/roots/test-intl/refs.po rename to tests/roots/test-intl/xx/LC_MESSAGES/refs.po diff --git a/tests/roots/test-intl/refs_inconsistency.po b/tests/roots/test-intl/xx/LC_MESSAGES/refs_inconsistency.po similarity index 100% rename from tests/roots/test-intl/refs_inconsistency.po rename to tests/roots/test-intl/xx/LC_MESSAGES/refs_inconsistency.po diff --git a/tests/roots/test-intl/refs_python_domain.po b/tests/roots/test-intl/xx/LC_MESSAGES/refs_python_domain.po similarity index 100% rename from tests/roots/test-intl/refs_python_domain.po rename to tests/roots/test-intl/xx/LC_MESSAGES/refs_python_domain.po diff --git a/tests/roots/test-intl/role_xref.po b/tests/roots/test-intl/xx/LC_MESSAGES/role_xref.po similarity index 100% rename from tests/roots/test-intl/role_xref.po rename to tests/roots/test-intl/xx/LC_MESSAGES/role_xref.po diff --git a/tests/roots/test-intl/rubric.po b/tests/roots/test-intl/xx/LC_MESSAGES/rubric.po similarity index 100% rename from tests/roots/test-intl/rubric.po rename to tests/roots/test-intl/xx/LC_MESSAGES/rubric.po diff --git a/tests/roots/test-intl/section.po b/tests/roots/test-intl/xx/LC_MESSAGES/section.po similarity index 100% rename from tests/roots/test-intl/section.po rename to tests/roots/test-intl/xx/LC_MESSAGES/section.po diff --git a/tests/roots/test-intl/seealso.po b/tests/roots/test-intl/xx/LC_MESSAGES/seealso.po similarity index 100% rename from tests/roots/test-intl/seealso.po rename to tests/roots/test-intl/xx/LC_MESSAGES/seealso.po diff --git a/tests/roots/test-intl/sphinx.po b/tests/roots/test-intl/xx/LC_MESSAGES/sphinx.po similarity index 100% rename from tests/roots/test-intl/sphinx.po rename to tests/roots/test-intl/xx/LC_MESSAGES/sphinx.po diff --git a/tests/roots/test-intl/table.po b/tests/roots/test-intl/xx/LC_MESSAGES/table.po similarity index 100% rename from tests/roots/test-intl/table.po rename to tests/roots/test-intl/xx/LC_MESSAGES/table.po diff --git a/tests/roots/test-intl/topic.po b/tests/roots/test-intl/xx/LC_MESSAGES/topic.po similarity index 100% rename from tests/roots/test-intl/topic.po rename to tests/roots/test-intl/xx/LC_MESSAGES/topic.po diff --git a/tests/roots/test-intl/versionchange.po b/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po similarity index 100% rename from tests/roots/test-intl/versionchange.po rename to tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po diff --git a/tests/roots/test-intl/warnings.po b/tests/roots/test-intl/xx/LC_MESSAGES/warnings.po similarity index 100% rename from tests/roots/test-intl/warnings.po rename to tests/roots/test-intl/xx/LC_MESSAGES/warnings.po diff --git a/tests/test_application.py b/tests/test_application.py index 87ee72f63..8c48e7500 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -7,6 +7,9 @@ :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ + +from unittest.mock import Mock + import pytest from docutils import nodes @@ -58,20 +61,13 @@ def test_extension_in_blacklist(app, status, warning): @pytest.mark.sphinx(testroot='add_source_parser') -@pytest.mark.filterwarnings('ignore:The config variable "source_parsers"') -@pytest.mark.filterwarnings('ignore:app.add_source_parser\\(\\) does not support suffix') def test_add_source_parser(app, status, warning): - assert set(app.config.source_suffix) == set(['.rst', '.md', '.test']) + assert set(app.config.source_suffix) == {'.rst', '.test'} # .rst; only in :confval:`source_suffix` assert '.rst' not in app.registry.get_source_parsers() assert app.registry.source_suffix['.rst'] is None - # .md; configured by :confval:`source_suffix` and :confval:`source_parsers` - assert '.md' in app.registry.get_source_parsers() - assert app.registry.source_suffix['.md'] == '.md' - assert app.registry.get_source_parsers()['.md'].__name__ == 'DummyMarkdownParser' - # .test; configured by API assert app.registry.source_suffix['.test'] == 'test' assert 'test' in app.registry.get_source_parsers() @@ -113,3 +109,22 @@ def test_add_is_parallel_allowed(app, status, warning): "for parallel reading, assuming it isn't - please ") in warning.getvalue() app.extensions.pop('write_serial') warning.truncate(0) # reset warnings + + +@pytest.mark.sphinx('dummy', testroot='root') +def test_build_specific(app): + app.builder.build = Mock() + filenames = [app.srcdir / 'index.txt', # normal + app.srcdir / 'images', # without suffix + app.srcdir / 'notfound.txt', # not found + app.srcdir / 'img.png', # unknown suffix + '/index.txt', # external file + app.srcdir / 'subdir', # directory + app.srcdir / 'subdir/includes.txt', # file on subdir + app.srcdir / 'subdir/../subdir/excluded.txt'] # not normalized + app.build(False, filenames) + + expected = ['index', 'img.png', 'subdir/includes', 'subdir/excluded'] + app.builder.build.assert_called_with(expected, + method='specific', + summary='4 source files given on command line') diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 42a1f4f3e..cfb62d1ba 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -9,7 +9,6 @@ :license: BSD, see LICENSE for details. """ -import re import platform import sys from warnings import catch_warnings @@ -17,10 +16,7 @@ from warnings import catch_warnings import pytest from docutils.statemachine import ViewList -from sphinx.ext.autodoc import ( - ModuleLevelDocumenter, cut_lines, between, ALL, - merge_autodoc_default_flags, Options -) +from sphinx.ext.autodoc import ModuleLevelDocumenter, cut_lines, between, ALL, Options from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options from sphinx.testing.util import SphinxTestApp, Struct # NOQA from sphinx.util import logging @@ -517,13 +513,11 @@ def test_docstring_processing(): app.disconnect(lid) -@pytest.mark.usefixtures('setup_test') -def test_new_documenter(): - logging.setup(app, app._status, app._warning) - +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_new_documenter(app): class MyDocumenter(ModuleLevelDocumenter): objtype = 'integer' - directivetype = 'data' + directivetype = 'integer' priority = 100 @classmethod @@ -535,17 +529,19 @@ def test_new_documenter(): app.add_autodocumenter(MyDocumenter) - def assert_result_contains(item, objtype, name, **kw): - app._warning.truncate(0) - inst = app.registry.documenters[objtype](directive, name) - inst.generate(**kw) - # print '\n'.join(directive.result) - assert app._warning.getvalue() == '' - assert item in directive.result - del directive.result[:] - - options.members = ['integer'] - assert_result_contains('.. py:data:: integer', 'module', 'target') + options = {"members": 'integer'} + actual = do_autodoc(app, 'module', 'target', options) + assert list(actual) == [ + '', + '.. py:module:: target', + '', + '', + '.. py:integer:: integer', + ' :module: target', + '', + ' documentation for the integer', + ' ' + ] @pytest.mark.usefixtures('setup_test') @@ -583,23 +579,29 @@ def test_attrgetter_using(): assert_getter_works('class', 'target.Class', Class, ['meth', 'inheritedmeth']) -@pytest.mark.usefixtures('setup_test') -def test_generate(): - def assert_result_contains(item, objtype, name, **kw): - inst = app.registry.documenters[objtype](directive, name) - inst.generate(**kw) - assert item in directive.result - del directive.result[:] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_py_module(app, warning): + # without py:module + actual = do_autodoc(app, 'method', 'Class.meth') + assert list(actual) == [] + assert ("don't know which module to import for autodocumenting 'Class.meth'" + in warning.getvalue()) - # test auto and given content mixing - directive.env.ref_context['py:module'] = 'target' - assert_result_contains(' Function.', 'method', 'Class.meth') - add_content = ViewList() - add_content.append('Content.', '', 0) - assert_result_contains(' Function.', 'method', - 'Class.meth', more_content=add_content) - assert_result_contains(' Content.', 'method', - 'Class.meth', more_content=add_content) + # with py:module + app.env.ref_context['py:module'] = 'target' + warning.truncate(0) + + actual = do_autodoc(app, 'method', 'Class.meth') + assert list(actual) == [ + '', + '.. py:method:: Class.meth()', + ' :module: target', + '', + ' Function.', + ' ' + ] + assert ("don't know which module to import for autodocumenting 'Class.meth'" + not in warning.getvalue()) @pytest.mark.sphinx('html', testroot='ext-autodoc') @@ -1517,27 +1519,6 @@ def test_partialmethod(app): assert list(actual) == expected -@pytest.mark.sphinx('html', testroot='ext-autodoc') -@pytest.mark.filterwarnings('ignore:autodoc_default_flags is now deprecated.') -def test_merge_autodoc_default_flags1(app): - app.config.autodoc_default_flags = ['members', 'undoc-members'] - merge_autodoc_default_flags(app, app.config) - assert app.config.autodoc_default_options == {'members': None, - 'undoc-members': None} - - -@pytest.mark.sphinx('html', testroot='ext-autodoc') -@pytest.mark.filterwarnings('ignore:autodoc_default_flags is now deprecated.') -def test_merge_autodoc_default_flags2(app): - app.config.autodoc_default_flags = ['members', 'undoc-members'] - app.config.autodoc_default_options = {'members': 'this,that,order', - 'inherited-members': 'this'} - merge_autodoc_default_flags(app, app.config) - assert app.config.autodoc_default_options == {'members': None, - 'undoc-members': None, - 'inherited-members': 'this'} - - @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_default_options(app): # no settings diff --git a/tests/test_build.py b/tests/test_build.py index 8072906a2..caa4ad354 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -10,8 +10,8 @@ import sys from textwrap import dedent +from unittest import mock -import mock import pytest from docutils import nodes @@ -54,10 +54,10 @@ def nonascii_srcdir(request, rootdir, sphinx_test_tempdir): # note: this test skips building docs for some builders because they have independent testcase. -# (html, epub, latex, texinfo and manpage) +# (html, changes, epub, latex, texinfo and manpage) @pytest.mark.parametrize( "buildername", - ['dirhtml', 'singlehtml', 'text', 'changes', 'xml', 'pseudoxml', 'linkcheck'], + ['dirhtml', 'singlehtml', 'text', 'xml', 'pseudoxml', 'linkcheck'], ) @mock.patch('sphinx.builders.linkcheck.requests.head', side_effect=request_session_head) diff --git a/tests/test_build_changes.py b/tests/test_build_changes.py new file mode 100644 index 000000000..911dcd0d1 --- /dev/null +++ b/tests/test_build_changes.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +""" + test_build_changes + ~~~~~~~~~~~~~~~~~~ + + Test the ChangesBuilder class. + + :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import pytest + + +@pytest.mark.sphinx('changes', testroot='changes') +def test_build(app): + app.build() + + # TODO: Use better checking of html content + htmltext = (app.outdir / 'changes.html').text() + assert 'New in version 0.6: Some funny stuff.' in htmltext + assert 'Changed in version 0.6: Even more funny stuff.' in htmltext + assert 'Deprecated since version 0.6: Boring stuff.' in htmltext + + path_html = ( + 'Path: deprecated: Deprecated since version 0.6:' + ' So, that was a bad idea it turns out.') + assert path_html in htmltext + + malloc_html = ( + 'Test_Malloc: changed: Changed in version 0.6:' + ' Can now be replaced with a different allocator.') + assert malloc_html in htmltext + + +@pytest.mark.sphinx( + 'changes', testroot='changes', srcdir='changes-none', + confoverrides={'version': '0.7', 'release': '0.7b1'}) +def test_no_changes(app, status): + app.build() + + assert 'no changes in version 0.7.' in status.getvalue() + assert not (app.outdir / 'changes.html').exists() diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 77c86962e..1dbf05a4a 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -391,8 +391,8 @@ def test_html4_output(app, status, warning): (".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r"1"), (".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r"2"), (".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"), - (".//a[@class='reference internal'][@href='#bar'][@id='id4']", r"\[bar\]"), - (".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']", r"\[baz_qux\]"), + (".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r"\[bar\]"), + (".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r"\[baz_qux\]"), (".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"4"), (".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r"5"), (".//a[@class='fn-backref'][@href='#id1']", r"1"), @@ -1254,6 +1254,15 @@ def test_html_inventory(app): 'The basic Sphinx documentation for testing') +@pytest.mark.sphinx('html', testroot='images', confoverrides={'html_sourcelink_suffix': ''}) +def test_html_anchor_for_figure(app): + app.builder.build_all() + content = (app.outdir / 'index.html').text() + assert ('

The caption of pic' + '

' + in content) + + @pytest.mark.sphinx('html', testroot='directives-raw') def test_html_raw_directive(app, status, warning): app.builder.build_all() diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 0cafdadd7..f02394cf1 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -1220,16 +1220,28 @@ def test_latex_raw_directive(app, status, warning): @pytest.mark.sphinx('latex', testroot='images') -def test_latex_remote_images(app, status, warning): +def test_latex_images(app, status, warning): app.builder.build_all() result = (app.outdir / 'python.tex').text(encoding='utf8') + + # images are copied assert '\\sphinxincludegraphics{{python-logo}.png}' in result assert (app.outdir / 'python-logo.png').exists() + + # not found images assert '\\sphinxincludegraphics{{NOT_EXIST}.PNG}' not in result assert ('WARNING: Could not fetch remote image: ' 'http://example.com/NOT_EXIST.PNG [404]' in warning.getvalue()) + # an image having target + assert ('\\sphinxhref{https://www.sphinx-doc.org/}' + '{\\sphinxincludegraphics{{rimg}.png}}\n\n' in result) + + # a centerized image having target + assert ('\\sphinxhref{https://www.python.org/}{{\\hspace*{\\fill}' + '\\sphinxincludegraphics{{rimg}.png}\\hspace*{\\fill}}}\n\n' in result) + @pytest.mark.sphinx('latex', testroot='latex-index') def test_latex_index(app, status, warning): @@ -1397,3 +1409,13 @@ def test_includegraphics_oversized(app, status, warning): print(status.getvalue()) print(warning.getvalue()) compile_latex_document(app) + + +@pytest.mark.sphinx('latex', testroot='index_on_title') +def test_index_on_title(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'python.tex').text(encoding='utf8') + assert ('\\chapter{Test for index in top level title}\n' + '\\label{\\detokenize{contents:test-for-index-in-top-level-title}}' + '\\index{index@\\spxentry{index}}\n' + in result) diff --git a/tests/test_build_manpage.py b/tests/test_build_manpage.py index 663db8439..17a2f7eb8 100644 --- a/tests/test_build_manpage.py +++ b/tests/test_build_manpage.py @@ -30,6 +30,27 @@ def test_all(app, status, warning): assert 'Footnotes' not in content +@pytest.mark.sphinx('man', testroot='directive-code') +def test_captioned_code_block(app, status, warning): + app.builder.build_all() + content = (app.outdir / 'python.1').text() + + assert ('.sp\n' + 'caption \\fItest\\fP rb\n' + '.INDENT 0.0\n' + '.INDENT 3.5\n' + '.sp\n' + '.nf\n' + '.ft C\n' + 'def ruby?\n' + ' false\n' + 'end\n' + '.ft P\n' + '.fi\n' + '.UNINDENT\n' + '.UNINDENT\n' in content) + + def test_default_man_pages(): config = Config({'project': 'STASI™ Documentation', 'author': "Wolfgang Schäuble & G'Beckstein", diff --git a/tests/test_builder.py b/tests/test_builder.py index 35197a8ef..fa64f0c1f 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -28,7 +28,7 @@ def test_incremental_reading(app): # second reading updated = app.builder.read() - assert set(updated) == set(['index', 'new']) + assert set(updated) == {'index', 'new'} assert 'autodoc' not in app.env.all_docs assert 'autodoc' not in app.env.found_docs @@ -44,4 +44,4 @@ def test_incremental_reading_for_missing_files(app): # "index" is listed up to updated because it contains references # to nonexisting downloadable or image files - assert set(updated) == set(['index']) + assert set(updated) == {'index'} diff --git a/tests/test_catalogs.py b/tests/test_catalogs.py index 14fca84d5..1a14d46e6 100644 --- a/tests/test_catalogs.py +++ b/tests/test_catalogs.py @@ -17,18 +17,19 @@ from sphinx.testing.util import find_files @pytest.fixture def setup_test(app_params): srcdir = app_params.kwargs['srcdir'] - locale_dir = srcdir / 'locale' + src_locale_dir = srcdir / 'xx' / 'LC_MESSAGES' + dest_locale_dir = srcdir / 'locale' # copy all catalogs into locale layout directory - for po in find_files(srcdir, '.po'): - copy_po = (locale_dir / 'en' / 'LC_MESSAGES' / po) + for po in find_files(src_locale_dir, '.po'): + copy_po = (dest_locale_dir / 'en' / 'LC_MESSAGES' / po) if not copy_po.parent.exists(): copy_po.parent.makedirs() - shutil.copy(srcdir / po, copy_po) + shutil.copy(src_locale_dir / po, copy_po) yield # delete remnants left over after failed build - locale_dir.rmtree(True) + dest_locale_dir.rmtree(True) (srcdir / '_build').rmtree(True) @@ -42,10 +43,10 @@ def test_compile_all_catalogs(app, status, warning): locale_dir = app.srcdir / 'locale' catalog_dir = locale_dir / app.config.language / 'LC_MESSAGES' - expect = set([ + expect = { x.replace('.po', '.mo') for x in find_files(catalog_dir, '.po') - ]) + } actual = set(find_files(catalog_dir, '.mo')) assert actual # not empty assert actual == expect @@ -66,7 +67,7 @@ def test_compile_specific_catalogs(app, status, warning): actual_on_boot = get_actual() # sphinx.mo might be included app.builder.compile_specific_catalogs([app.srcdir / 'admonitions.txt']) actual = get_actual() - actual_on_boot - assert actual == set(['admonitions.mo']) + assert actual == {'admonitions.mo'} @pytest.mark.usefixtures('setup_test') @@ -79,10 +80,10 @@ def test_compile_update_catalogs(app, status, warning): locale_dir = app.srcdir / 'locale' catalog_dir = locale_dir / app.config.language / 'LC_MESSAGES' - expect = set([ + expect = { x.replace('.po', '.mo') for x in find_files(catalog_dir, '.po') - ]) + } actual = set(find_files(catalog_dir, '.mo')) assert actual # not empty assert actual == expect diff --git a/tests/test_config.py b/tests/test_config.py index fadf7d6c4..a5da0d6ec 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,7 +8,9 @@ :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -import mock + +from unittest import mock + import pytest import sphinx @@ -257,7 +259,7 @@ def test_conf_warning_message(logger, name, default, annotation, actual, message config.add(name, default, False, annotation or ()) config.init_values() check_confval_types(None, config) - logger.warning.assert_called() + assert logger.warning.called assert logger.warning.call_args[0][0] == message @@ -276,7 +278,7 @@ def test_check_enum_failed(logger): config.add('value', 'default', False, ENUM('default', 'one', 'two')) config.init_values() check_confval_types(None, config) - logger.warning.assert_called() + assert logger.warning.called @mock.patch("sphinx.config.logger") @@ -294,4 +296,4 @@ def test_check_enum_for_list_failed(logger): config.add('value', 'default', False, ENUM('default', 'one', 'two')) config.init_values() check_confval_types(None, config) - logger.warning.assert_called() + assert logger.warning.called diff --git a/tests/test_directive_code.py b/tests/test_directive_code.py index 8627820b9..2e1a1fde2 100644 --- a/tests/test_directive_code.py +++ b/tests/test_directive_code.py @@ -565,3 +565,52 @@ def test_code_block_highlighted(app, status, warning): assert codeblocks[1]['language'] == 'python2' assert codeblocks[2]['language'] == 'python3' assert codeblocks[3]['language'] == 'python2' + + +@pytest.mark.sphinx('html', testroot='directive-code') +def test_linenothreshold(app, status, warning): + app.builder.build(['linenothreshold']) + html = (app.outdir / 'linenothreshold.html').text() + + lineos_head = '
'
+    lineos_tail = '
' + + # code-block using linenothreshold + _, matched, html = html.partition(lineos_head + + '1\n' + '2\n' + '3\n' + '4\n' + '5\n' + '6' + lineos_tail) + assert matched + + # code-block not using linenothreshold + html, matched, _ = html.partition(lineos_head + + '1\n' + '2' + lineos_tail) + assert not matched + + # literal include using linenothreshold + _, matched, html = html.partition(lineos_head + + ' 1\n' + ' 2\n' + ' 3\n' + ' 4\n' + ' 5\n' + ' 6\n' + ' 7\n' + ' 8\n' + ' 9\n' + '10\n' + '11\n' + '12\n' + '13' + lineos_tail) + assert matched + + # literal include not using linenothreshold + html, matched, _ = html.partition(lineos_head + + '1\n' + '2\n' + '3' + lineos_tail) + assert not matched diff --git a/tests/test_directive_other.py b/tests/test_directive_other.py index cbbebee5c..376b9cd69 100644 --- a/tests/test_directive_other.py +++ b/tests/test_directive_other.py @@ -10,25 +10,12 @@ import pytest from docutils import nodes -from docutils.core import publish_doctree from sphinx import addnodes -from sphinx.io import SphinxStandaloneReader -from sphinx.parsers import RSTParser +from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node -def parse(app, docname, text): - app.env.temp_data['docname'] = docname - parser = RSTParser() - parser.set_application(app) - return publish_doctree(text, app.srcdir / docname + '.rst', - reader=SphinxStandaloneReader(app), - parser=parser, - settings_overrides={'env': app.env, - 'gettext_compact': True}) - - @pytest.mark.sphinx(testroot='toctree-glob') def test_toctree(app): text = (".. toctree::\n" @@ -38,7 +25,7 @@ def test_toctree(app): " baz\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'foo'), (None, 'bar/index'), (None, 'baz')], @@ -55,7 +42,7 @@ def test_relative_toctree(app): " ../quux\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'bar/index', text) + doctree = restructuredtext.parse(app, text, 'bar/index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'bar/bar_1'), (None, 'bar/bar_2'), (None, 'bar/bar_3'), @@ -72,7 +59,7 @@ def test_toctree_urls_and_titles(app): " The BAR \n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[('Sphinx', 'https://www.sphinx-doc.org/'), @@ -89,7 +76,7 @@ def test_toctree_glob(app): " *\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'baz'), (None, 'foo'), (None, 'quux')], @@ -103,7 +90,7 @@ def test_toctree_glob(app): " *\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'foo'), (None, 'baz'), (None, 'quux')], @@ -117,7 +104,7 @@ def test_toctree_glob(app): " foo\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'baz'), (None, 'foo'), (None, 'quux'), (None, 'foo')], @@ -132,7 +119,7 @@ def test_toctree_glob_and_url(app): " https://example.com/?q=sphinx\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'https://example.com/?q=sphinx')], @@ -147,7 +134,7 @@ def test_toctree_twice(app): " foo\n") app.env.find_files(app.config, app.builder) - doctree = parse(app, 'index', text) + doctree = restructuredtext.parse(app, text, 'index') assert_node(doctree, [nodes.document, nodes.compound, addnodes.toctree]) assert_node(doctree[0][0], entries=[(None, 'foo'), (None, 'foo')], diff --git a/tests/test_directive_patch.py b/tests/test_directive_patch.py new file mode 100644 index 000000000..4f61f2d0b --- /dev/null +++ b/tests/test_directive_patch.py @@ -0,0 +1,54 @@ +""" + test_directive_patch + ~~~~~~~~~~~~~~~~~~~ + + Test the patched directives. + + :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from docutils import nodes + +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + + +def test_code_directive(app): + # normal case + text = ('.. code::\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="default", highlight_args={}) + + # with language + text = ('.. code:: python\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="python", highlight_args={}) + + # :number-lines: option + text = ('.. code:: python\n' + ' :number-lines:\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="python", linenos=True, highlight_args={}) + + # :number-lines: option + text = ('.. code:: python\n' + ' :number-lines: 5\n' + '\n' + ' print("hello world")\n') + + doctree = restructuredtext.parse(app, text) + assert_node(doctree, [nodes.document, nodes.literal_block, 'print("hello world")']) + assert_node(doctree[0], language="python", linenos=True, highlight_args={'linenostart': 5}) diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 4d7d3e592..46019b4a9 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -9,7 +9,6 @@ """ import re -import sys import pytest @@ -152,9 +151,8 @@ def test_expressions(): exprCheck(p + "'\\x0A'", t + "10") exprCheck(p + "'\\u0a42'", t + "2626") exprCheck(p + "'\\u0A42'", t + "2626") - if sys.maxunicode > 65535: - exprCheck(p + "'\\U0001f34c'", t + "127820") - exprCheck(p + "'\\U0001F34C'", t + "127820") + exprCheck(p + "'\\U0001f34c'", t + "127820") + exprCheck(p + "'\\U0001F34C'", t + "127820") # TODO: user-defined lit exprCheck('(... + Ns)', '(... + Ns)', id4='flpl2Ns') @@ -196,6 +194,8 @@ def test_expressions(): exprCheck('new int()', 'nw_ipiE') exprCheck('new int(5, 42)', 'nw_ipiL5EL42EE') exprCheck('::new int', 'nw_iE') + exprCheck('new int{}', 'nw_iilE') + exprCheck('new int{5, 42}', 'nw_iilL5EL42EE') # delete-expression exprCheck('delete p', 'dl1p') exprCheck('delete [] p', 'da1p') @@ -675,6 +675,40 @@ def test_template_args(): {2: "I0E21enable_if_not_array_t"}) +def test_initializers(): + idsMember = {1: 'v__T', 2:'1v'} + idsFunction = {1: 'f__T', 2: '1f1T'} + idsTemplate = {2: 'I_1TE1fv', 4: 'I_1TE1fvv'} + # no init + check('member', 'T v', idsMember) + check('function', 'void f(T v)', idsFunction) + check('function', 'template void f()', idsTemplate) + # with '=', assignment-expression + check('member', 'T v = 42', idsMember) + check('function', 'void f(T v = 42)', idsFunction) + check('function', 'template void f()', idsTemplate) + # with '=', braced-init + check('member', 'T v = {}', idsMember) + check('function', 'void f(T v = {})', idsFunction) + check('function', 'template void f()', idsTemplate) + check('member', 'T v = {42, 42, 42}', idsMember) + check('function', 'void f(T v = {42, 42, 42})', idsFunction) + check('function', 'template void f()', idsTemplate) + check('member', 'T v = {42, 42, 42,}', idsMember) + check('function', 'void f(T v = {42, 42, 42,})', idsFunction) + check('function', 'template void f()', idsTemplate) + check('member', 'T v = {42, 42, args...}', idsMember) + check('function', 'void f(T v = {42, 42, args...})', idsFunction) + check('function', 'template void f()', idsTemplate) + # without '=', braced-init + check('member', 'T v{}', idsMember) + check('member', 'T v{42, 42, 42}', idsMember) + check('member', 'T v{42, 42, 42,}', idsMember) + check('member', 'T v{42, 42, args...}', idsMember) + # other + check('member', 'T v = T{}', idsMember) + + def test_attributes(): # style: C++ check('member', '[[]] int f', {1: 'f__i', 2: '1f'}) diff --git a/tests/test_domain_js.py b/tests/test_domain_js.py index 174a431bf..613623ee5 100644 --- a/tests/test_domain_js.py +++ b/tests/test_domain_js.py @@ -8,9 +8,10 @@ :license: BSD, see LICENSE for details. """ +from unittest.mock import Mock + import pytest from docutils import nodes -from mock import Mock from sphinx import addnodes from sphinx.domains.javascript import JavaScriptDomain diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index ff6387101..fb6e70914 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -8,12 +8,18 @@ :license: BSD, see LICENSE for details. """ +from unittest.mock import Mock + import pytest from docutils import nodes -from mock import Mock from sphinx import addnodes +from sphinx.addnodes import ( + desc, desc_addname, desc_annotation, desc_content, desc_name, desc_optional, + desc_parameter, desc_parameterlist, desc_returns, desc_signature +) from sphinx.domains.python import py_sig_re, _pseudo_parse_arglist, PythonDomain +from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node @@ -202,3 +208,85 @@ def test_get_full_qualified_name(): kwargs = {'py:module': 'module1', 'py:class': 'Class'} node = nodes.reference(reftarget='func', **kwargs) assert domain.get_full_qualified_name(node) == 'module1.Class.func' + + +def test_pyfunction_signature(app): + text = ".. py:function:: hello(name: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "hello"], + desc_parameterlist, + [desc_returns, "str"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", noindex=False) + assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, "name: str"]) + + +def test_optional_pyfunction_signature(app): + text = ".. py:function:: compile(source [, filename [, symbol]]) -> ast object" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "compile"], + desc_parameterlist, + [desc_returns, "ast object"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", noindex=False) + assert_node(doctree[1][0][1], + ([desc_parameter, "source"], + [desc_optional, ([desc_parameter, "filename"], + [desc_optional, desc_parameter, "symbol"])])) + + +def test_pyexception_signature(app): + text = ".. py:exception:: exceptions.IOError" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "exception "], + [desc_addname, "exceptions."], + [desc_name, "IOError"])], + desc_content)])) + assert_node(doctree[1], desc, desctype="exception", + domain="py", objtype="exception", noindex=False) + + +def test_exceptions_module_is_ignored(app): + text = (".. py:exception:: IOError\n" + " :module: exceptions\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "exception "], + [desc_name, "IOError"])], + desc_content)])) + assert_node(doctree[1], desc, desctype="exception", + domain="py", objtype="exception", noindex=False) + + +def test_pydata_signature(app): + text = (".. py:data:: version\n" + " :annotation: = 1\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "version"], + [desc_annotation, " = 1"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="data", + domain="py", objtype="data", noindex=False) + + +def test_pyobject_prefix(app): + text = (".. py:class:: Foo\n" + "\n" + " .. py:method:: Foo.say\n" + " .. py:method:: FooBar.say") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, "class "], + [desc_name, "Foo"])], + [desc_content, (addnodes.index, + desc, + addnodes.index, + desc)])])) + assert doctree[1][1][1].astext().strip() == 'say' # prefix is stripped + assert doctree[1][1][3].astext().strip() == 'FooBar.say' # not stripped diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py index dda8a4313..15daeeea6 100644 --- a/tests/test_domain_std.py +++ b/tests/test_domain_std.py @@ -8,7 +8,8 @@ :license: BSD, see LICENSE for details. """ -import mock +from unittest import mock + from docutils import nodes from sphinx.domains.std import StandardDomain diff --git a/tests/test_environment.py b/tests/test_environment.py index df0aa20b0..15562536f 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -25,21 +25,20 @@ def test_images(app): htmlbuilder.imgpath = 'dummy' htmlbuilder.post_process_images(tree) assert set(htmlbuilder.images.keys()) == \ - set(['subdir/img.png', 'img.png', 'subdir/simg.png', 'svgimg.svg', - 'img.foo.png']) + {'subdir/img.png', 'img.png', 'subdir/simg.png', 'svgimg.svg', 'img.foo.png'} assert set(htmlbuilder.images.values()) == \ - set(['img.png', 'img1.png', 'simg.png', 'svgimg.svg', 'img.foo.png']) + {'img.png', 'img1.png', 'simg.png', 'svgimg.svg', 'img.foo.png'} latexbuilder = LaTeXBuilder(app) latexbuilder.set_environment(app.env) latexbuilder.init() latexbuilder.post_process_images(tree) assert set(latexbuilder.images.keys()) == \ - set(['subdir/img.png', 'subdir/simg.png', 'img.png', 'img.pdf', - 'svgimg.pdf', 'img.foo.png']) + {'subdir/img.png', 'subdir/simg.png', 'img.png', 'img.pdf', + 'svgimg.pdf', 'img.foo.png'} assert set(latexbuilder.images.values()) == \ - set(['img.pdf', 'img.png', 'img1.png', 'simg.png', - 'svgimg.pdf', 'img.foo.png']) + {'img.pdf', 'img.png', 'img1.png', 'simg.png', + 'svgimg.pdf', 'img.foo.png'} @pytest.mark.sphinx('dummy') diff --git a/tests/test_environment_indexentries.py b/tests/test_environment_indexentries.py index 4475fb273..62e4ffb79 100644 --- a/tests/test_environment_indexentries.py +++ b/tests/test_environment_indexentries.py @@ -9,8 +9,7 @@ """ from collections import namedtuple - -import mock +from unittest import mock from sphinx import locale from sphinx.environment.adapters.indexentries import IndexEntries diff --git a/tests/test_environment_toctree.py b/tests/test_environment_toctree.py index c490dcedf..9d880d92c 100644 --- a/tests/test_environment_toctree.py +++ b/tests/test_environment_toctree.py @@ -75,11 +75,11 @@ def test_process_doc(app): # other collections assert app.env.toc_num_entries['index'] == 6 assert app.env.toctree_includes['index'] == ['foo', 'bar', 'baz'] - assert app.env.files_to_rebuild['foo'] == set(['index']) - assert app.env.files_to_rebuild['bar'] == set(['index']) - assert app.env.files_to_rebuild['baz'] == set(['index']) + assert app.env.files_to_rebuild['foo'] == {'index'} + assert app.env.files_to_rebuild['bar'] == {'index'} + assert app.env.files_to_rebuild['baz'] == {'index'} assert app.env.glob_toctrees == set() - assert app.env.numbered_toctrees == set(['index']) + assert app.env.numbered_toctrees == {'index'} # qux has no section title assert len(app.env.tocs['qux']) == 0 diff --git a/tests/test_ext_autodoc_importer.py b/tests/test_ext_autodoc_mock.py similarity index 93% rename from tests/test_ext_autodoc_importer.py rename to tests/test_ext_autodoc_mock.py index 08920cc7e..79a2782d3 100644 --- a/tests/test_ext_autodoc_importer.py +++ b/tests/test_ext_autodoc_mock.py @@ -1,6 +1,6 @@ """ - test_ext_autodoc_importer - ~~~~~~~~~~~~~~~~~~~~~~~~~ + test_ext_autodoc_mock + ~~~~~~~~~~~~~~~~~~~~~ Test the autodoc extension. @@ -13,11 +13,11 @@ import sys import pytest -from sphinx.ext.autodoc.importer import _MockModule, _MockObject, mock +from sphinx.ext.autodoc.mock import _MockModule, _MockObject, mock def test_MockModule(): - mock = _MockModule('mocked_module', None) + mock = _MockModule('mocked_module') assert isinstance(mock.some_attr, _MockObject) assert isinstance(mock.some_method, _MockObject) assert isinstance(mock.attr1.attr2, _MockObject) diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index ef6161cac..0d1282691 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -11,11 +11,13 @@ import sys from io import StringIO import os +from unittest.mock import Mock import pytest from sphinx.ext.autosummary import mangle_signature, import_by_name, extract_summary from sphinx.testing.util import etree_parse +from sphinx.util.docutils import new_document html_warnfile = StringIO() @@ -58,8 +60,6 @@ def test_mangle_signature(): def test_extract_summary(capsys): - from sphinx.util.docutils import new_document - from mock import Mock settings = Mock(language_code='', id_prefix='', auto_id_prefix='', diff --git a/tests/test_ext_coverage.py b/tests/test_ext_coverage.py index d02d65feb..73181909d 100644 --- a/tests/test_ext_coverage.py +++ b/tests/test_ext_coverage.py @@ -37,7 +37,7 @@ def test_build(app, status, warning): undoc_py, undoc_c = pickle.loads((app.outdir / 'undoc.pickle').bytes()) assert len(undoc_c) == 1 # the key is the full path to the header file, which isn't testable - assert list(undoc_c.values())[0] == set([('function', 'Py_SphinxTest')]) + assert list(undoc_c.values())[0] == {('function', 'Py_SphinxTest')} assert 'autodoc_target' in undoc_py assert 'funcs' in undoc_py['autodoc_target'] diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py index 9e5d3e60f..03b5bb689 100644 --- a/tests/test_ext_inheritance_diagram.py +++ b/tests/test_ext_inheritance_diagram.py @@ -121,7 +121,7 @@ def test_import_classes(rootdir): # all of classes in the module classes = import_classes('sphinx.application', None) - assert set(classes) == set([Sphinx, TemplateBridge]) + assert set(classes) == {Sphinx, TemplateBridge} # specified class in the module classes = import_classes('sphinx.application.Sphinx', None) diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 45684123f..93bf16834 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -11,8 +11,8 @@ import os import unittest from io import BytesIO +from unittest import mock -import mock import pytest import requests from docutils import nodes diff --git a/tests/test_ext_napoleon.py b/tests/test_ext_napoleon.py index 9127109d9..19eb536fa 100644 --- a/tests/test_ext_napoleon.py +++ b/tests/test_ext_napoleon.py @@ -10,9 +10,7 @@ """ from collections import namedtuple -from unittest import TestCase - -import mock +from unittest import TestCase, mock from sphinx.application import Sphinx from sphinx.ext.napoleon import _process_docstring, _skip_member, Config, setup diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 71ac1870e..86ded7d89 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -12,9 +12,7 @@ from collections import namedtuple from inspect import cleandoc from textwrap import dedent -from unittest import TestCase - -import mock +from unittest import TestCase, mock from sphinx.ext.napoleon import Config from sphinx.ext.napoleon.docstring import GoogleDocstring, NumpyDocstring @@ -38,7 +36,7 @@ class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))): __slots__ = () def __new__(cls, attr1, attr2=None): - return super(NamedtupleSubclass, cls).__new__(cls, attr1, attr2) + return super().__new__(cls, attr1, attr2) class BaseDocstringTest(TestCase): diff --git a/tests/test_ext_todo.py b/tests/test_ext_todo.py index 2ce7ac95e..1ce030208 100644 --- a/tests/test_ext_todo.py +++ b/tests/test_ext_todo.py @@ -30,23 +30,19 @@ def test_todo(app, status, warning): # check todolist content = (app.outdir / 'index.html').text() - html = ('

Todo

\n' - '

todo in foo

') - assert re.search(html, content, re.S) + assert ('

Todo

\n' + '

todo in foo

') in content - html = ('

Todo

\n' - '

todo in bar

') - assert re.search(html, content, re.S) + assert ('

Todo

\n' + '

todo in bar

') in content # check todo content = (app.outdir / 'foo.html').text() - html = ('

Todo

\n' - '

todo in foo

') - assert re.search(html, content, re.S) + assert ('

Todo

\n' + '

todo in foo

') in content - html = ('

Todo

\n' - '

todo in param field

') - assert re.search(html, content, re.S) + assert ('

Todo

\n' + '

todo in param field

') in content # check emitted warnings assert 'WARNING: TODO entry found: todo in foo' in warning.getvalue() @@ -54,9 +50,9 @@ def test_todo(app, status, warning): # check handled event assert len(todos) == 3 - assert set(todo[1].astext() for todo in todos) == {'todo in foo', - 'todo in bar', - 'todo in param field'} + assert {todo[1].astext() for todo in todos} == {'todo in foo', + 'todo in bar', + 'todo in param field'} @pytest.mark.sphinx('html', testroot='ext-todo', freshenv=True, @@ -72,19 +68,16 @@ def test_todo_not_included(app, status, warning): # check todolist content = (app.outdir / 'index.html').text() - html = ('

Todo

\n' - '

todo in foo

') - assert not re.search(html, content, re.S) + assert ('

Todo

\n' + '

todo in foo

') not in content - html = ('

Todo

\n' - '

todo in bar

') - assert not re.search(html, content, re.S) + assert ('

Todo

\n' + '

todo in bar

') not in content # check todo content = (app.outdir / 'foo.html').text() - html = ('

Todo

\n' - '

todo in foo

') - assert not re.search(html, content, re.S) + assert ('

Todo

\n' + '

todo in foo

') not in content # check emitted warnings assert 'WARNING: TODO entry found: todo in foo' in warning.getvalue() @@ -92,9 +85,9 @@ def test_todo_not_included(app, status, warning): # check handled event assert len(todos) == 3 - assert set(todo[1].astext() for todo in todos) == {'todo in foo', - 'todo in bar', - 'todo in param field'} + assert {todo[1].astext() for todo in todos} == {'todo in foo', + 'todo in bar', + 'todo in param field'} @pytest.mark.sphinx('latex', testroot='ext-todo', freshenv=True, @@ -114,14 +107,15 @@ def test_todo_valid_link(app, status, warning): # Look for the link to foo. Note that there are two of them because the # source document uses todolist twice. We could equally well look for links # to bar. - link = r'\{\\hyperref\[\\detokenize\{(.*?foo.*?)}]\{\\sphinxcrossref{' \ - r'\\sphinxstyleemphasis{original entry}}}}' + link = (r'{\\hyperref\[\\detokenize{(.*?foo.*?)}]{\\sphinxcrossref{' + r'\\sphinxstyleemphasis{original entry}}}}') m = re.findall(link, content) assert len(m) == 4 target = m[0] # Look for the targets of this link. - labels = [m for m in re.findall(r'\\label\{([^}]*)}', content) if m == target] + labels = re.findall(r'\\label{\\detokenize{([^}]*)}}', content) + matched = [l for l in labels if l == target] # If everything is correct we should have exactly one target. - assert len(labels) == 1 + assert len(matched) == 1 diff --git a/tests/test_highlighting.py b/tests/test_highlighting.py index efe2871c8..fca51d02f 100644 --- a/tests/test_highlighting.py +++ b/tests/test_highlighting.py @@ -8,7 +8,8 @@ :license: BSD, see LICENSE for details. """ -import mock +from unittest import mock + from pygments.formatters.html import HtmlFormatter from pygments.lexer import RegexLexer from pygments.token import Text, Name diff --git a/tests/test_intl.py b/tests/test_intl.py index 367409fd9..a052266b8 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -41,31 +41,21 @@ def write_mo(pathname, po): return mofile.write_mo(f, po) -@pytest.fixture -def build_mo(): - def builder(srcdir): - """ - :param str srcdir: app.srcdir - """ - srcdir = path(srcdir) - for dirpath, dirs, files in os.walk(srcdir): - dirpath = path(dirpath) - for f in [f for f in files if f.endswith('.po')]: - po = dirpath / f - mo = srcdir / 'xx' / 'LC_MESSAGES' / ( - os.path.relpath(po[:-3], srcdir) + '.mo') - if not mo.parent.exists(): - mo.parent.makedirs() - - if not mo.exists() or mo.stat().st_mtime < po.stat().st_mtime: - # compile .mo file only if needed - write_mo(mo, read_po(po)) - return builder - - @pytest.fixture(autouse=True) -def setup_intl(app_params, build_mo): - build_mo(app_params.kwargs['srcdir']) +def setup_intl(app_params): + srcdir = path(app_params.kwargs['srcdir']) + for dirpath, dirs, files in os.walk(srcdir): + dirpath = path(dirpath) + for f in [f for f in files if f.endswith('.po')]: + po = dirpath / f + mo = srcdir / 'xx' / 'LC_MESSAGES' / ( + os.path.relpath(po[:-3], srcdir) + '.mo') + if not mo.parent.exists(): + mo.parent.makedirs() + + if not mo.exists() or mo.stat().st_mtime < po.stat().st_mtime: + # compile .mo file only if needed + write_mo(mo, read_po(po)) @pytest.fixture(autouse=True) @@ -296,7 +286,7 @@ def test_text_glossary_term_inconsistencies(app, warning): def test_gettext_section(app): app.build() # --- section - expect = read_po(app.srcdir / 'section.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'section.po') actual = read_po(app.outdir / 'section.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -309,7 +299,7 @@ def test_text_section(app): app.build() # --- section result = (app.outdir / 'section.txt').text() - expect = read_po(app.srcdir / 'section.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'section.po') for expect_msg in [m for m in expect if m.id]: assert expect_msg.string in result @@ -445,7 +435,7 @@ def test_text_admonitions(app): def test_gettext_toctree(app): app.build() # --- toctree - expect = read_po(app.srcdir / 'index.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'index.po') actual = read_po(app.outdir / 'index.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -457,7 +447,7 @@ def test_gettext_toctree(app): def test_gettext_table(app): app.build() # --- toctree - expect = read_po(app.srcdir / 'table.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'table.po') actual = read_po(app.outdir / 'table.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -470,7 +460,7 @@ def test_text_table(app): app.build() # --- toctree result = (app.outdir / 'table.txt').text() - expect = read_po(app.srcdir / 'table.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'table.po') for expect_msg in [m for m in expect if m.id]: assert expect_msg.string in result @@ -481,7 +471,7 @@ def test_text_table(app): def test_gettext_topic(app): app.build() # --- topic - expect = read_po(app.srcdir / 'topic.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'topic.po') actual = read_po(app.outdir / 'topic.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -494,7 +484,7 @@ def test_text_topic(app): app.build() # --- topic result = (app.outdir / 'topic.txt').text() - expect = read_po(app.srcdir / 'topic.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'topic.po') for expect_msg in [m for m in expect if m.id]: assert expect_msg.string in result @@ -505,7 +495,7 @@ def test_text_topic(app): def test_gettext_definition_terms(app): app.build() # --- definition terms: regression test for #2198, #2205 - expect = read_po(app.srcdir / 'definition_terms.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'definition_terms.po') actual = read_po(app.outdir / 'definition_terms.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -517,7 +507,7 @@ def test_gettext_definition_terms(app): def test_gettext_glossary_terms(app, warning): app.build() # --- glossary terms: regression test for #1090 - expect = read_po(app.srcdir / 'glossary_terms.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'glossary_terms.po') actual = read_po(app.outdir / 'glossary_terms.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -531,7 +521,7 @@ def test_gettext_glossary_terms(app, warning): def test_gettext_glossary_term_inconsistencies(app): app.build() # --- glossary term inconsistencies: regression test for #1090 - expect = read_po(app.srcdir / 'glossary_terms_inconsistency.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'glossary_terms_inconsistency.po') actual = read_po(app.outdir / 'glossary_terms_inconsistency.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -543,7 +533,7 @@ def test_gettext_glossary_term_inconsistencies(app): def test_gettext_literalblock(app): app.build() # --- gettext builder always ignores ``only`` directive - expect = read_po(app.srcdir / 'literalblock.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'literalblock.po') actual = read_po(app.outdir / 'literalblock.pot') for expect_msg in [m for m in expect if m.id]: if len(expect_msg.id.splitlines()) == 1: @@ -559,7 +549,7 @@ def test_gettext_literalblock(app): def test_gettext_buildr_ignores_only_directive(app): app.build() # --- gettext builder always ignores ``only`` directive - expect = read_po(app.srcdir / 'only.po') + expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'only.po') actual = read_po(app.outdir / 'only.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -568,7 +558,7 @@ def test_gettext_buildr_ignores_only_directive(app): @sphinx_intl # use individual shared_result directory to avoid "incompatible doctree" error @pytest.mark.sphinx(testroot='builder-gettext-dont-rebuild-mo') -def test_gettext_dont_rebuild_mo(make_app, app_params, build_mo): +def test_gettext_dont_rebuild_mo(make_app, app_params): # --- don't rebuild by .mo mtime def get_number_of_update_targets(app_): app_.env.find_files(app_.config, app_.builder) @@ -579,7 +569,6 @@ def test_gettext_dont_rebuild_mo(make_app, app_params, build_mo): # phase1: build document with non-gettext builder and generate mo file in srcdir app0 = make_app('dummy', *args, **kwargs) - build_mo(app0.srcdir) app0.build() assert (app0.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo').exists() # Since it is after the build, the number of documents to be updated is 0 @@ -617,6 +606,8 @@ def test_html_meta(app): assert expected_expr in result expected_expr = '' assert expected_expr in result + expected_expr = '

HIDDEN TOC

' + assert expected_expr in result @sphinx_intl @@ -829,8 +820,7 @@ def test_xml_footnote_backlinks(app): para0 = secs[0].findall('paragraph') refs0 = para0[0].findall('footnote_reference') - refid2id = dict([ - (r.attrib.get('refid'), r.attrib.get('ids')) for r in refs0]) + refid2id = {r.attrib.get('refid'): r.attrib.get('ids') for r in refs0} footnote0 = secs[0].findall('footnote') for footnote in footnote0: diff --git a/tests/test_roles.py b/tests/test_roles.py index 5a04b55ad..8f0e546b6 100644 --- a/tests/test_roles.py +++ b/tests/test_roles.py @@ -8,14 +8,17 @@ :license: BSD, see LICENSE for details. """ -from docutils import nodes -from mock import Mock +from unittest.mock import Mock -from sphinx.roles import emph_literal_role +from docutils import nodes + +from sphinx.roles import EmphasizedLiteral from sphinx.testing.util import assert_node def test_samp(): + emph_literal_role = EmphasizedLiteral() + # normal case text = 'print 1+{variable}' ret, msg = emph_literal_role('samp', text, text, 0, Mock()) diff --git a/tests/test_util.py b/tests/test_util.py index 0926096f4..44a41dca1 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -10,16 +10,16 @@ import os import tempfile +from unittest.mock import patch import pytest -from mock import patch import sphinx -from sphinx.errors import PycodeError +from sphinx.errors import ExtensionError, PycodeError from sphinx.testing.util import strip_escseq from sphinx.util import ( SkipProgressMessage, display_chunk, encode_uri, ensuredir, get_module_source, - parselinenos, progress_message, status_iterator, xmlname_checker + import_object, parselinenos, progress_message, status_iterator, xmlname_checker ) from sphinx.util import logging @@ -71,6 +71,26 @@ def test_get_module_source(): get_module_source('itertools') +def test_import_object(): + module = import_object('sphinx') + assert module.__name__ == 'sphinx' + + module = import_object('sphinx.application') + assert module.__name__ == 'sphinx.application' + + obj = import_object('sphinx.application.Sphinx') + assert obj.__name__ == 'Sphinx' + + with pytest.raises(ExtensionError) as exc: + import_object('sphinx.unknown_module') + assert exc.value.args[0] == 'Could not import sphinx.unknown_module' + + with pytest.raises(ExtensionError) as exc: + import_object('sphinx.unknown_module', 'my extension') + assert exc.value.args[0] == ('Could not import sphinx.unknown_module ' + '(needed for my extension)') + + @pytest.mark.sphinx('dummy') @patch('sphinx.util.console._tw', 40) # terminal width = 40 def test_status_iterator(app, status, warning): diff --git a/tests/test_util_fileutil.py b/tests/test_util_fileutil.py index 635559efa..7e0d261bd 100644 --- a/tests/test_util_fileutil.py +++ b/tests/test_util_fileutil.py @@ -8,7 +8,7 @@ :license: BSD, see LICENSE for details. """ -import mock +from unittest import mock from sphinx.jinja2glue import BuiltinTemplateLoader from sphinx.util.fileutil import copy_asset, copy_asset_file diff --git a/tests/test_util_i18n.py b/tests/test_util_i18n.py index b25e29575..4ca39d5ef 100644 --- a/tests/test_util_i18n.py +++ b/tests/test_util_i18n.py @@ -56,117 +56,6 @@ def test_catalog_write_mo(tempdir): assert read_mo(f) is not None -def test_get_catalogs_for_xx(tempdir): - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES').makedirs() - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test2.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test3.pot').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub').makedirs() - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test4.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test5.po').write_text('#') - (tempdir / 'loc1' / 'en' / 'LC_MESSAGES').makedirs() - (tempdir / 'loc1' / 'en' / 'LC_MESSAGES' / 'test6.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_ALL').makedirs() - (tempdir / 'loc1' / 'xx' / 'LC_ALL' / 'test7.po').write_text('#') - - catalogs = i18n.find_catalog_source_files([tempdir / 'loc1'], 'xx', force_all=False) - domains = set(c.domain for c in catalogs) - assert domains == set([ - 'test1', - 'test2', - 'sub/test4', - 'sub/test5', - ]) - - -def test_get_catalogs_for_en(tempdir): - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES').makedirs() - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'xx_dom.po').write_text('#') - (tempdir / 'loc1' / 'en' / 'LC_MESSAGES').makedirs() - (tempdir / 'loc1' / 'en' / 'LC_MESSAGES' / 'en_dom.po').write_text('#') - - catalogs = i18n.find_catalog_source_files([tempdir / 'loc1'], 'en', force_all=False) - domains = set(c.domain for c in catalogs) - assert domains == set(['en_dom']) - - -def test_get_catalogs_with_non_existent_locale(tempdir): - catalogs = i18n.find_catalog_source_files([tempdir / 'loc1'], 'xx') - assert not catalogs - - catalogs = i18n.find_catalog_source_files([tempdir / 'loc1'], None) - assert not catalogs - - -def test_get_catalogs_with_non_existent_locale_dirs(): - catalogs = i18n.find_catalog_source_files(['dummy'], 'xx') - assert not catalogs - - -def test_get_catalogs_for_xx_without_outdated(tempdir): - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES').makedirs() - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test1.mo').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test2.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test2.mo').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test3.pot').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test3.mo').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub').makedirs() - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test4.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test4.mo').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test5.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test5.mo').write_text('#') - - catalogs = i18n.find_catalog_source_files([tempdir / 'loc1'], 'xx', force_all=False) - assert not catalogs - - catalogs = i18n.find_catalog_source_files([tempdir / 'loc1'], 'xx', force_all=True) - domains = set(c.domain for c in catalogs) - assert domains == set([ - 'test1', - 'test2', - 'sub/test4', - 'sub/test5', - ]) - - -def test_get_catalogs_from_multiple_locale_dirs(tempdir): - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES').makedirs() - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#') - (tempdir / 'loc2' / 'xx' / 'LC_MESSAGES').makedirs() - (tempdir / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#') - (tempdir / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test2.po').write_text('#') - - catalogs = i18n.find_catalog_source_files([tempdir / 'loc1', tempdir / 'loc2'], 'xx') - domains = sorted(c.domain for c in catalogs) - assert domains == ['test1', 'test1', 'test2'] - - -@pytest.mark.filterwarnings('ignore:gettext_compact argument') -def test_get_catalogs_with_compact(tempdir): - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES').makedirs() - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test2.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub').makedirs() - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test3.po').write_text('#') - (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test4.po').write_text('#') - - catalogs = i18n.find_catalog_source_files([tempdir / 'loc1'], 'xx', gettext_compact=True) - domains = set(c.domain for c in catalogs) - assert domains == set(['test1', 'test2', 'sub/test3', 'sub/test4']) - - -def test_get_catalogs_excluded(tempdir): - (tempdir / 'loc1' / 'en' / 'LC_MESSAGES' / '.git').makedirs() - (tempdir / 'loc1' / 'en' / 'LC_MESSAGES' / 'en_dom.po').write_text('#') - (tempdir / 'loc1' / 'en' / 'LC_MESSAGES' / '.git' / 'no_no.po').write_text('#') - - catalogs = i18n.find_catalog_source_files( - [tempdir / 'loc1'], 'en', force_all=False, excluded=lambda path: '.git' in path) - domains = set(c.domain for c in catalogs) - assert domains == set(['en_dom']) - - def test_format_date(): date = datetime.date(2016, 2, 7) @@ -255,3 +144,47 @@ def test_get_filename_for_language(app): app.env.config.figure_language_filename = '{root}.{invalid}{ext}' with pytest.raises(SphinxError): i18n.get_image_filename_for_language('foo.png', app.env) + + +def test_CatalogRepository(tempdir): + (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES').makedirs() + (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#') + (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'test2.po').write_text('#') + (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub').makedirs() + (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test3.po').write_text('#') + (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / 'sub' / 'test4.po').write_text('#') + (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / '.dotdir').makedirs() + (tempdir / 'loc1' / 'xx' / 'LC_MESSAGES' / '.dotdir' / 'test5.po').write_text('#') + (tempdir / 'loc1' / 'yy' / 'LC_MESSAGES').makedirs() + (tempdir / 'loc1' / 'yy' / 'LC_MESSAGES' / 'test6.po').write_text('#') + (tempdir / 'loc2' / 'xx' / 'LC_MESSAGES').makedirs() + (tempdir / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#') + (tempdir / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test7.po').write_text('#') + + # for language xx + repo = i18n.CatalogRepository(tempdir, ['loc1', 'loc2'], 'xx', 'utf-8') + assert list(repo.locale_dirs) == [str(tempdir / 'loc1'), + str(tempdir / 'loc2')] + assert all(isinstance(c, i18n.CatalogInfo) for c in repo.catalogs) + assert sorted(c.domain for c in repo.catalogs) == ['sub/test3', 'sub/test4', + 'test1', 'test1', 'test2', 'test7'] + + # for language yy + repo = i18n.CatalogRepository(tempdir, ['loc1', 'loc2'], 'yy', 'utf-8') + assert sorted(c.domain for c in repo.catalogs) == ['test6'] + + # unknown languages + repo = i18n.CatalogRepository(tempdir, ['loc1', 'loc2'], 'zz', 'utf-8') + assert sorted(c.domain for c in repo.catalogs) == [] + + # no languages + repo = i18n.CatalogRepository(tempdir, ['loc1', 'loc2'], None, 'utf-8') + assert sorted(c.domain for c in repo.catalogs) == [] + + # unknown locale_dirs + repo = i18n.CatalogRepository(tempdir, ['loc3'], None, 'utf-8') + assert sorted(c.domain for c in repo.catalogs) == [] + + # no locale_dirs + repo = i18n.CatalogRepository(tempdir, [], None, 'utf-8') + assert sorted(c.domain for c in repo.catalogs) == [] diff --git a/tests/test_util_images.py b/tests/test_util_images.py index 5e0f2801c..dbe155035 100644 --- a/tests/test_util_images.py +++ b/tests/test_util_images.py @@ -20,20 +20,15 @@ PDF_FILENAME = 'img.pdf' TXT_FILENAME = 'index.txt' -@pytest.fixture(scope='module') -def testroot(rootdir): - return rootdir / 'test-root' - - -def test_get_image_size(testroot): - assert get_image_size(testroot / GIF_FILENAME) == (200, 181) - assert get_image_size(testroot / PNG_FILENAME) == (200, 181) - assert get_image_size(testroot / PDF_FILENAME) is None - assert get_image_size(testroot / TXT_FILENAME) is None +def test_get_image_size(rootdir): + assert get_image_size(rootdir / 'test-root' / GIF_FILENAME) == (200, 181) + assert get_image_size(rootdir / 'test-root' / PNG_FILENAME) == (200, 181) + assert get_image_size(rootdir / 'test-root' / PDF_FILENAME) is None + assert get_image_size(rootdir / 'test-root' / TXT_FILENAME) is None @pytest.mark.filterwarnings('ignore:The content argument') -def test_guess_mimetype(testroot): +def test_guess_mimetype(): # guess by filename assert guess_mimetype('img.png') == 'image/png' assert guess_mimetype('img.jpg') == 'image/jpeg' @@ -42,24 +37,9 @@ def test_guess_mimetype(testroot): assert guess_mimetype('no_extension') is None assert guess_mimetype('IMG.PNG') == 'image/png' - # guess by content - assert guess_mimetype(content=(testroot / GIF_FILENAME).bytes()) == 'image/gif' - assert guess_mimetype(content=(testroot / PNG_FILENAME).bytes()) == 'image/png' - assert guess_mimetype(content=(testroot / PDF_FILENAME).bytes()) is None - assert guess_mimetype(content=(testroot / TXT_FILENAME).bytes()) is None - assert guess_mimetype(content=(testroot / TXT_FILENAME).bytes(), - default='text/plain') == 'text/plain' - - # the priority of params: filename > content > default - assert guess_mimetype('img.png', - content=(testroot / GIF_FILENAME).bytes(), - default='text/plain') == 'image/png' - assert guess_mimetype('no_extension', - content=(testroot / GIF_FILENAME).bytes(), - default='text/plain') == 'image/gif' - assert guess_mimetype('no_extension', - content=(testroot / TXT_FILENAME).bytes(), - default='text/plain') == 'text/plain' + # default parameter is used when no extension + assert guess_mimetype('img.png', 'text/plain') == 'image/png' + assert guess_mimetype('no_extension', 'text/plain') == 'text/plain' def test_get_image_extension(): diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 6cb2a4b1b..ba2bb7501 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -352,7 +352,7 @@ def test_set_sorting(): def test_set_sorting_fallback(): - set_ = set((None, 1)) + set_ = {None, 1} description = inspect.object_description(set_) assert description in ("{1, None}", "{None, 1}") diff --git a/utils/jssplitter_generator.py b/utils/jssplitter_generator.py index 255bc0a98..360ce7d15 100644 --- a/utils/jssplitter_generator.py +++ b/utils/jssplitter_generator.py @@ -79,7 +79,7 @@ function splitQuery(query) { } ''' % (fold(singles, ','), fold(ranges, '],')) -js_test_src = u''' +js_test_src = ''' // This is regression test for https://github.com/sphinx-doc/sphinx/issues/3150 // generated by compat_regexp_generator.py // it needs node.js for testing diff --git a/utils/release-checklist b/utils/release-checklist index 84cbb3829..12cbe6381 100644 --- a/utils/release-checklist +++ b/utils/release-checklist @@ -12,7 +12,8 @@ for stable releases * ``git commit -am 'Bump to X.Y.Z final'`` * ``make clean`` * ``python setup.py release bdist_wheel sdist`` -* ``twine upload dist/ --sign --identity [your GPG key]`` +* ``twine check dist/Sphinx-X.Y.Z*`` +* ``twine upload dist/Sphinx-X.Y.Z* --sign --identity [your GPG key]`` * open https://pypi.org/project/Sphinx/ and check there are no obvious errors * ``git tag vX.Y.Z`` * ``python utils/bump_version.py --in-develop X.Y.Zb0`` (ex. 1.5.3b0) @@ -39,7 +40,8 @@ for first beta releases * ``git commit -am 'Bump to X.Y.0 beta1'`` * ``make clean`` * ``python setup.py release bdist_wheel sdist`` -* ``twine upload dist/ --sign --identity [your GPG key]`` +* ``twine check dist/Sphinx-X.Y.Z*`` +* ``twine upload dist/Sphinx-X.Y.Z* --sign --identity [your GPG key]`` * open https://pypi.org/project/Sphinx/ and check there are no obvious errors * ``git tag vX.Y.0b1`` * ``python utils/bump_version.py --in-develop X.Y.0b2`` (ex. 1.6.0b2) @@ -69,7 +71,8 @@ for other beta releases * ``git commit -am 'Bump to X.Y.0 betaN'`` * ``make clean`` * ``python setup.py release bdist_wheel sdist`` -* ``twine upload dist/ --sign --identity [your GPG key]`` +* ``twine check dist/Sphinx-X.Y.Z*`` +* ``twine upload dist/Sphinx-X.Y.Z* --sign --identity [your GPG key]`` * open https://pypi.org/project/Sphinx/ and check there are no obvious errors * ``git tag vX.Y.0bN`` * ``python utils/bump_version.py --in-develop X.Y.0bM`` (ex. 1.6.0b3) @@ -98,7 +101,8 @@ for major releases * ``git commit -am 'Bump to X.Y.0 final'`` * ``make clean`` * ``python setup.py release bdist_wheel sdist`` -* ``twine upload dist/ --sign --identity [your GPG key]`` +* ``twine check dist/Sphinx-X.Y.Z*`` +* ``twine upload dist/Sphinx-X.Y.Z* --sign --identity [your GPG key]`` * open https://pypi.org/project/Sphinx/ and check there are no obvious errors * ``git tag vX.Y.0`` * ``python utils/bump_version.py --in-develop X.Y.1b0`` (ex. 1.6.1b0)